Merge branch 'master' into binary-protocal

This commit is contained in:
Jonathan Kelley authored and GitHub committed 2024-01-04 10:17:10 -08:00
commit a2f44be2a2
76 files changed
+1918 -625

No files matched your search

+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
# args: --path packages/cli
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
+1
View File
@@ -4,6 +4,7 @@
/dist
Cargo.lock
.DS_Store
/examples/assets/test_video.mp4
.vscode/*
!.vscode/settings.json
+1
View File
@@ -133,3 +133,4 @@ fern = { version = "0.6.0", features = ["colored"] }
env_logger = "0.10.0"
simple_logger = "4.0.0"
thiserror = { workspace = true }
http-range = "0.1.5"
+1 -1
View File
@@ -161,7 +161,7 @@ So... Dioxus is great, but why won't it work for me?
## Contributing
- Check out the website [section on contributing](https://dioxuslabs.com/learn/0.4/contributing).
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
- Join the discord and ask questions!
- [Join](https://discord.gg/XgGxMSkvUM) the discord and ask questions!
<a href="https://github.com/dioxuslabs/dioxus/graphs/contributors">
+4 -7
View File
@@ -53,8 +53,7 @@ fn app(cx: Scope) -> Element {
};
cx.render(rsx! (
div {
style: "{CONTAINER_STYLE}",
div { style: "{CONTAINER_STYLE}",
div {
style: "{RECT_STYLE}",
// focusing is necessary to catch keyboard events
@@ -62,7 +61,7 @@ fn app(cx: Scope) -> Element {
onmousemove: move |event| log_event(Event::MouseMove(event)),
onclick: move |event| log_event(Event::MouseClick(event)),
ondblclick: move |event| log_event(Event::MouseDoubleClick(event)),
ondoubleclick: move |event| log_event(Event::MouseDoubleClick(event)),
onmousedown: move |event| log_event(Event::MouseDown(event)),
onmouseup: move |event| log_event(Event::MouseUp(event)),
@@ -77,9 +76,7 @@ fn app(cx: Scope) -> Element {
"Hover, click, type or scroll to see the info down below"
}
div {
events.read().iter().map(|event| rsx!( div { "{event:?}" } ))
},
},
div { events.read().iter().map(|event| rsx!( div { "{event:?}" } )) }
}
))
}
+1 -7
View File
@@ -27,13 +27,7 @@ fn app(cx: Scope) -> Element {
button {
onclick: move |_| {
let dom = VirtualDom::new_with_props(compose, ComposeProps {
app_tx: tx.clone()
});
// this returns a weak reference to the other window
// Be careful not to keep a strong reference to the other window or it will never be dropped
// and the window will never close.
let dom = VirtualDom::new_with_props(compose, ComposeProps { app_tx: tx.clone() });
window.new_window(dom, Default::default());
},
"Click to compose a new email"
+29
View File
@@ -0,0 +1,29 @@
use dioxus::prelude::*;
use dioxus_desktop::wry::http::Response;
use dioxus_desktop::{use_asset_handler, AssetRequest};
use std::path::Path;
fn main() {
dioxus_desktop::launch(app);
}
fn app(cx: Scope) -> Element {
use_asset_handler(cx, |request: &AssetRequest| {
let path = request.path().to_path_buf();
async move {
if path != Path::new("logo.png") {
return None;
}
let image_data: &[u8] = include_bytes!("./assets/logo.png");
Some(Response::new(image_data.into()))
}
});
cx.render(rsx! {
div {
img {
src: "logo.png"
}
}
})
}
+1 -1
View File
@@ -35,7 +35,7 @@ frameworks = ["WebKit"]
[dependencies]
anyhow = "1.0.56"
log = "0.4.11"
wry = "0.31.0"
wry = "0.34.0"
dioxus = { path = "../../packages/dioxus" }
dioxus-desktop = { path = "../../packages/desktop", features = [
"tokio_runtime",
+48 -41
View File
@@ -48,11 +48,8 @@ pub fn app(cx: Scope<()>) -> Element {
cx.render(rsx! {
section { class: "todoapp",
style { include_str!("./assets/todomvc.css") }
TodoHeader {
todos: todos,
}
section {
class: "main",
TodoHeader { todos: todos }
section { class: "main",
if !todos.is_empty() {
rsx! {
input {
@@ -103,31 +100,34 @@ pub fn TodoHeader<'a>(cx: Scope<'a, TodoHeaderProps<'a>>) -> Element {
cx.render(rsx! {
header { class: "header",
h1 {"todos"}
input {
class: "new-todo",
placeholder: "What needs to be done?",
value: "{draft}",
autofocus: "true",
oninput: move |evt| {
draft.set(evt.value.clone());
},
onkeydown: move |evt| {
if evt.key() == Key::Enter && !draft.is_empty() {
cx.props.todos.make_mut().insert(
**todo_id,
TodoItem {
id: **todo_id,
checked: false,
contents: draft.to_string(),
},
);
*todo_id.make_mut() += 1;
draft.set("".to_string());
h1 { "todos" }
input {
class: "new-todo",
placeholder: "What needs to be done?",
value: "{draft}",
autofocus: "true",
oninput: move |evt| {
draft.set(evt.value.clone());
},
onkeydown: move |evt| {
if evt.key() == Key::Enter && !draft.is_empty() {
cx.props
.todos
.make_mut()
.insert(
**todo_id,
TodoItem {
id: **todo_id,
checked: false,
contents: draft.to_string(),
},
);
*todo_id.make_mut() += 1;
draft.set("".to_string());
}
}
}
}
}
})
}
@@ -146,8 +146,7 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
let editing = if **is_editing { "editing" } else { "" };
cx.render(rsx!{
li {
class: "{completed} {editing}",
li { class: "{completed} {editing}",
div { class: "view",
input {
class: "toggle",
@@ -160,14 +159,16 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
}
label {
r#for: "cbg-{todo.id}",
ondblclick: move |_| is_editing.set(true),
ondoubleclick: move |_| is_editing.set(true),
prevent_default: "onclick",
"{todo.contents}"
}
button {
class: "destroy",
onclick: move |_| { cx.props.todos.make_mut().remove(&todo.id); },
prevent_default: "onclick",
onclick: move |_| {
cx.props.todos.make_mut().remove(&todo.id);
},
prevent_default: "onclick"
}
}
is_editing.then(|| rsx!{
@@ -213,15 +214,15 @@ pub fn ListFooter<'a>(cx: Scope<'a, ListFooterProps<'a>>) -> Element {
cx.render(rsx! {
footer { class: "footer",
span { class: "todo-count",
strong {"{active_todo_count} "}
span {"{active_todo_text} left"}
strong { "{active_todo_count} " }
span { "{active_todo_text} left" }
}
ul { class: "filters",
for (state, state_text, url) in [
(FilterState::All, "All", "#/"),
(FilterState::Active, "Active", "#/active"),
(FilterState::Completed, "Completed", "#/completed"),
] {
for (state , state_text , url) in [
(FilterState::All, "All", "#/"),
(FilterState::Active, "Active", "#/active"),
(FilterState::Completed, "Completed", "#/completed"),
] {
li {
a {
href: url,
@@ -250,8 +251,14 @@ pub fn PageFooter(cx: Scope) -> Element {
cx.render(rsx! {
footer { class: "info",
p { "Double-click to edit a todo" }
p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }}
p { "Part of ", a { href: "http://todomvc.com", "TodoMVC" }}
p {
"Created by "
a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }
}
p {
"Part of "
a { href: "http://todomvc.com", "TodoMVC" }
}
}
})
}
+184
View File
@@ -0,0 +1,184 @@
use dioxus::prelude::*;
use dioxus_desktop::wry::http;
use dioxus_desktop::wry::http::Response;
use dioxus_desktop::{use_asset_handler, AssetRequest};
use http::{header::*, response::Builder as ResponseBuilder, status::StatusCode};
use std::borrow::Cow;
use std::{io::SeekFrom, path::PathBuf};
use tokio::io::AsyncReadExt;
use tokio::io::AsyncSeekExt;
use tokio::io::AsyncWriteExt;
const VIDEO_PATH: &str = "./examples/assets/test_video.mp4";
fn main() {
let video_file = PathBuf::from(VIDEO_PATH);
if !video_file.exists() {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
println!("Downloading video file...");
let video_url =
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
let mut response = reqwest::get(video_url).await.unwrap();
let mut file = tokio::fs::File::create(&video_file).await.unwrap();
while let Some(chunk) = response.chunk().await.unwrap() {
file.write_all(&chunk).await.unwrap();
}
});
}
dioxus_desktop::launch(app);
}
fn app(cx: Scope) -> Element {
use_asset_handler(cx, move |request: &AssetRequest| {
let request = request.clone();
async move {
let video_file = PathBuf::from(VIDEO_PATH);
let mut file = tokio::fs::File::open(&video_file).await.unwrap();
let response: Option<Response<Cow<'static, [u8]>>> =
match get_stream_response(&mut file, &request).await {
Ok(response) => Some(response.map(Cow::Owned)),
Err(err) => {
eprintln!("Error: {}", err);
None
}
};
response
}
});
render! {
div { video { src: "test_video.mp4", autoplay: true, controls: true, width: 640, height: 480 } }
}
}
async fn get_stream_response(
asset: &mut (impl tokio::io::AsyncSeek + tokio::io::AsyncRead + Unpin + Send + Sync),
request: &AssetRequest,
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
// get stream length
let len = {
let old_pos = asset.stream_position().await?;
let len = asset.seek(SeekFrom::End(0)).await?;
asset.seek(SeekFrom::Start(old_pos)).await?;
len
};
let mut resp = ResponseBuilder::new().header(CONTENT_TYPE, "video/mp4");
// if the webview sent a range header, we need to send a 206 in return
// Actually only macOS and Windows are supported. Linux will ALWAYS return empty headers.
let http_response = if let Some(range_header) = request.headers().get("range") {
let not_satisfiable = || {
ResponseBuilder::new()
.status(StatusCode::RANGE_NOT_SATISFIABLE)
.header(CONTENT_RANGE, format!("bytes */{len}"))
.body(vec![])
};
// parse range header
let ranges = if let Ok(ranges) = http_range::HttpRange::parse(range_header.to_str()?, len) {
ranges
.iter()
// map the output back to spec range <start-end>, example: 0-499
.map(|r| (r.start, r.start + r.length - 1))
.collect::<Vec<_>>()
} else {
return Ok(not_satisfiable()?);
};
/// The Maximum bytes we send in one range
const MAX_LEN: u64 = 1000 * 1024;
if ranges.len() == 1 {
let &(start, mut end) = ranges.first().unwrap();
// check if a range is not satisfiable
//
// this should be already taken care of by HttpRange::parse
// but checking here again for extra assurance
if start >= len || end >= len || end < start {
return Ok(not_satisfiable()?);
}
// adjust end byte for MAX_LEN
end = start + (end - start).min(len - start).min(MAX_LEN - 1);
// calculate number of bytes needed to be read
let bytes_to_read = end + 1 - start;
// allocate a buf with a suitable capacity
let mut buf = Vec::with_capacity(bytes_to_read as usize);
// seek the file to the starting byte
asset.seek(SeekFrom::Start(start)).await?;
// read the needed bytes
asset.take(bytes_to_read).read_to_end(&mut buf).await?;
resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}"));
resp = resp.header(CONTENT_LENGTH, end + 1 - start);
resp = resp.status(StatusCode::PARTIAL_CONTENT);
resp.body(buf)
} else {
let mut buf = Vec::new();
let ranges = ranges
.iter()
.filter_map(|&(start, mut end)| {
// filter out unsatisfiable ranges
//
// this should be already taken care of by HttpRange::parse
// but checking here again for extra assurance
if start >= len || end >= len || end < start {
None
} else {
// adjust end byte for MAX_LEN
end = start + (end - start).min(len - start).min(MAX_LEN - 1);
Some((start, end))
}
})
.collect::<Vec<_>>();
let boundary = format!("{:x}", rand::random::<u64>());
let boundary_sep = format!("\r\n--{boundary}\r\n");
let boundary_closer = format!("\r\n--{boundary}\r\n");
resp = resp.header(
CONTENT_TYPE,
format!("multipart/byteranges; boundary={boundary}"),
);
for (end, start) in ranges {
// a new range is being written, write the range boundary
buf.write_all(boundary_sep.as_bytes()).await?;
// write the needed headers `Content-Type` and `Content-Range`
buf.write_all(format!("{CONTENT_TYPE}: video/mp4\r\n").as_bytes())
.await?;
buf.write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes())
.await?;
// write the separator to indicate the start of the range body
buf.write_all("\r\n".as_bytes()).await?;
// calculate number of bytes needed to be read
let bytes_to_read = end + 1 - start;
let mut local_buf = vec![0_u8; bytes_to_read as usize];
asset.seek(SeekFrom::Start(start)).await?;
asset.read_exact(&mut local_buf).await?;
buf.extend_from_slice(&local_buf);
}
// all ranges have been written, write the closing boundary
buf.write_all(boundary_closer.as_bytes()).await?;
resp.body(buf)
}
} else {
resp = resp.header(CONTENT_LENGTH, len);
let mut buf = Vec::with_capacity(len as usize);
asset.read_to_end(&mut buf).await?;
resp.body(buf)
};
http_response.map_err(Into::into)
}
+1
View File
@@ -83,6 +83,7 @@ dioxus-html = { workspace = true, features = ["hot-reload-context"] }
dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-hot-reload = { workspace = true }
interprocess-docfix = { version = "1.2.2" }
gitignore = "1.0.8"
[features]
default = []
+4 -4
View File
@@ -10,7 +10,7 @@ It handles building, bundling, development and publishing to simplify developmen
### Install the stable version (recommended)
```
```shell
cargo install dioxus-cli
```
@@ -20,7 +20,7 @@ To get the latest bug fixes and features, you can install the development versio
However, this is not fully tested.
That means you're probably going to have more bugs despite having the latest bug fixes.
```
```shell
cargo install --git https://github.com/DioxusLabs/dioxus dioxus-cli
```
@@ -29,7 +29,7 @@ and install it in Cargo's global binary directory (`~/.cargo/bin/` by default).
### Install from local folder
```
```shell
cargo install --path . --debug
```
@@ -40,7 +40,7 @@ It will be cloned from the [dioxus-template](https://github.com/DioxusLabs/dioxu
Alternatively, you can specify the template path:
```
```shell
dx create hello --template gh:dioxuslabs/dioxus-template
```
+18 -2
View File
@@ -93,6 +93,8 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
cmd
};
let cmd = cmd.args(&config.cargo_args);
let cmd = match executable {
ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
@@ -265,7 +267,6 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
let mut cmd = subprocess::Exec::cmd("cargo")
.cwd(&config.crate_dir)
.arg("build")
.arg("--quiet")
.arg("--message-format=json");
if config.release {
@@ -273,6 +274,8 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
}
if config.verbose {
cmd = cmd.arg("--verbose");
} else {
cmd = cmd.arg("--quiet");
}
if config.custom_profile.is_some() {
@@ -285,6 +288,14 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
cmd = cmd.arg("--features").arg(features_str);
}
if let Some(target) = &config.target {
cmd = cmd.arg("--target").arg(target);
}
let target_platform = config.target.as_deref().unwrap_or("");
cmd = cmd.args(&config.cargo_args);
let cmd = match &config.executable {
crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
@@ -302,12 +313,17 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
let mut res_path = match &config.executable {
crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => {
file_name = name.clone();
config.target_dir.join(release_type).join(name)
config
.target_dir
.join(target_platform)
.join(release_type)
.join(name)
}
crate::ExecutableType::Example(name) => {
file_name = name.clone();
config
.target_dir
.join(target_platform)
.join(release_type)
.join("examples")
.join(name)
+95 -80
View File
@@ -27,15 +27,17 @@ pub struct Autoformat {
impl Autoformat {
// Todo: autoformat the entire crate
pub async fn autoformat(self) -> Result<()> {
let Autoformat { check, raw, file } = self;
// Default to formatting the project
if self.raw.is_none() && self.file.is_none() {
if let Err(e) = autoformat_project(self.check).await {
if raw.is_none() && file.is_none() {
if let Err(e) = autoformat_project(check).await {
eprintln!("error formatting project: {}", e);
exit(1);
}
}
if let Some(raw) = self.raw {
if let Some(raw) = raw {
let indent = indentation_for(".")?;
if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0, indent) {
println!("{}", inner);
@@ -47,47 +49,90 @@ impl Autoformat {
}
// Format single file
if let Some(file) = self.file {
let file_content;
let indent;
if file == "-" {
indent = indentation_for(".")?;
let mut contents = String::new();
std::io::stdin().read_to_string(&mut contents)?;
file_content = Ok(contents);
} else {
indent = indentation_for(".")?;
file_content = fs::read_to_string(&file);
};
match file_content {
Ok(s) => {
let edits = dioxus_autofmt::fmt_file(&s, indent);
let out = dioxus_autofmt::apply_formats(&s, edits);
if file == "-" {
print!("{}", out);
} else {
match fs::write(&file, out) {
Ok(_) => {
println!("formatted {}", file);
}
Err(e) => {
eprintln!("failed to write formatted content to file: {}", e);
}
}
}
}
Err(e) => {
eprintln!("failed to open file: {}", e);
exit(1);
}
}
if let Some(file) = file {
refactor_file(file)?;
}
Ok(())
}
}
fn refactor_file(file: String) -> Result<(), Error> {
let indent = indentation_for(".")?;
let file_content = if file == "-" {
let mut contents = String::new();
std::io::stdin().read_to_string(&mut contents)?;
Ok(contents)
} else {
fs::read_to_string(&file)
};
let Ok(s) = file_content else {
eprintln!("failed to open file: {}", file_content.unwrap_err());
exit(1);
};
let edits = dioxus_autofmt::fmt_file(&s, indent);
let out = dioxus_autofmt::apply_formats(&s, edits);
if file == "-" {
print!("{}", out);
} else if let Err(e) = fs::write(&file, out) {
eprintln!("failed to write formatted content to file: {e}",);
} else {
println!("formatted {}", file);
}
Ok(())
}
fn get_project_files(config: &CrateConfig) -> Vec<PathBuf> {
let mut files = vec![];
let gitignore_path = config.crate_dir.join(".gitignore");
if gitignore_path.is_file() {
let gitigno = gitignore::File::new(gitignore_path.as_path()).unwrap();
if let Ok(git_files) = gitigno.included_files() {
let git_files = git_files
.into_iter()
.filter(|f| f.ends_with(".rs") && !is_target_dir(f));
files.extend(git_files)
};
} else {
collect_rs_files(&config.crate_dir, &mut files);
}
files
}
fn is_target_dir(file: &Path) -> bool {
let stripped = if let Ok(cwd) = std::env::current_dir() {
file.strip_prefix(cwd).unwrap_or(file)
} else {
file
};
if let Some(first) = stripped.components().next() {
first.as_os_str() == "target"
} else {
false
}
}
async fn format_file(
path: impl AsRef<Path>,
indent: IndentOptions,
) -> Result<usize, tokio::io::Error> {
let contents = tokio::fs::read_to_string(&path).await?;
let edits = dioxus_autofmt::fmt_file(&contents, indent);
let len = edits.len();
if !edits.is_empty() {
let out = dioxus_autofmt::apply_formats(&contents, edits);
tokio::fs::write(path, out).await?;
}
Ok(len)
}
/// Read every .rs file accessible when considering the .gitignore and try to format it
///
/// Runs using Tokio for multithreading, so it should be really really fast
@@ -96,8 +141,7 @@ impl Autoformat {
async fn autoformat_project(check: bool) -> Result<()> {
let crate_config = crate::CrateConfig::new(None)?;
let mut files_to_format = vec![];
collect_rs_files(&crate_config.crate_dir, &mut files_to_format);
let files_to_format = get_project_files(&crate_config);
if files_to_format.is_empty() {
return Ok(());
@@ -107,38 +151,17 @@ async fn autoformat_project(check: bool) -> Result<()> {
let counts = files_to_format
.into_iter()
.filter(|file| {
if file.components().any(|f| f.as_os_str() == "target") {
return false;
}
true
})
.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, _indent.clone());
let len = edits.len();
if !edits.is_empty() {
let out = dioxus_autofmt::apply_formats(&contents, edits);
tokio::fs::write(&path, out).await?;
}
Ok(len) as Result<usize, tokio::io::Error>
})
.await;
let path_clone = path.clone();
let res = tokio::spawn(format_file(path, indent.clone())).await;
match res {
Err(err) => {
eprintln!("error formatting file: {}\n{err}", _path.display());
eprintln!("error formatting file: {}\n{err}", path_clone.display());
None
}
Ok(Err(err)) => {
eprintln!("error formatting file: {}\n{err}", _path.display());
eprintln!("error formatting file: {}\n{err}", path_clone.display());
None
}
Ok(Ok(res)) => Some(res),
@@ -148,13 +171,7 @@ async fn autoformat_project(check: bool) -> Result<()> {
.collect::<Vec<_>>()
.await;
let files_formatted: usize = counts
.into_iter()
.map(|f| match f {
Some(res) => res,
_ => 0,
})
.sum();
let files_formatted: usize = counts.into_iter().flatten().sum();
if files_formatted > 0 && check {
eprintln!("{} files needed formatting", files_formatted);
@@ -207,26 +224,24 @@ fn indentation_for(file_or_dir: impl AsRef<Path>) -> Result<IndentOptions> {
))
}
fn collect_rs_files(folder: &Path, files: &mut Vec<PathBuf>) {
let Ok(folder) = folder.read_dir() else {
fn collect_rs_files(folder: &impl AsRef<Path>, files: &mut Vec<PathBuf>) {
if is_target_dir(folder.as_ref()) {
return;
}
let Ok(folder) = folder.as_ref().read_dir() else {
return;
};
// load the gitignore
for entry in folder {
let Ok(entry) = entry else {
continue;
};
let path = entry.path();
if path.is_dir() {
collect_rs_files(&path, files);
}
if let Some(ext) = path.extension() {
if ext == "rs" {
if ext == "rs" && !is_target_dir(&path) {
files.push(path);
}
}
+6
View File
@@ -37,6 +37,12 @@ impl Build {
.platform
.unwrap_or(crate_config.dioxus_config.application.default_platform);
if let Some(target) = self.build.target {
crate_config.set_target(target);
}
crate_config.set_cargo_args(self.build.cargo_args);
// #[cfg(feature = "plugin")]
// let _ = PluginManager::on_build_start(&crate_config, &platform);
+13 -2
View File
@@ -76,6 +76,12 @@ impl Bundle {
crate_config.set_profile(self.build.profile.unwrap());
}
if let Some(target) = &self.build.target {
crate_config.set_target(target.to_string());
}
crate_config.set_cargo_args(self.build.cargo_args);
// build the desktop app
build_desktop(&crate_config, false)?;
@@ -148,6 +154,11 @@ impl Bundle {
.collect(),
);
}
if let Some(target) = &self.build.target {
settings = settings.target(target.to_string());
}
let settings = settings.build();
// on macos we need to set CI=true (https://github.com/tauri-apps/tauri/issues/2567)
@@ -156,9 +167,9 @@ impl Bundle {
tauri_bundler::bundle::bundle_project(settings.unwrap()).unwrap_or_else(|err|{
#[cfg(target_os = "macos")]
panic!("Failed to bundle project: {}\nMake sure you have automation enabled in your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208) and full disk access enabled for your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208)", err);
panic!("Failed to bundle project: {:#?}\nMake sure you have automation enabled in your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208) and full disk access enabled for your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208)", err);
#[cfg(not(target_os = "macos"))]
panic!("Failed to bundle project: {}", err);
panic!("Failed to bundle project: {:#?}", err);
});
Ok(())
+24 -8
View File
@@ -6,10 +6,6 @@ use super::*;
/// Config options for the build system.
#[derive(Clone, Debug, Default, Deserialize, Parser)]
pub struct ConfigOptsBuild {
/// The index HTML file to drive the bundling process [default: index.html]
#[arg(long)]
pub target: Option<PathBuf>,
/// Build in release mode [default: false]
#[clap(long)]
#[serde(default)]
@@ -35,14 +31,18 @@ pub struct ConfigOptsBuild {
/// Space separated list of features to activate
#[clap(long)]
pub features: Option<Vec<String>>,
/// Rustc platform triple
#[clap(long)]
pub target: Option<String>,
/// Extra arguments passed to cargo build
#[clap(last = true)]
pub cargo_args: Vec<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Parser)]
pub struct ConfigOptsServe {
/// The index HTML file to drive the bundling process [default: index.html]
#[arg(short, long)]
pub target: Option<PathBuf>,
/// Port of dev server
#[clap(long)]
#[clap(default_value_t = 8080)]
@@ -89,6 +89,14 @@ pub struct ConfigOptsServe {
/// Space separated list of features to activate
#[clap(long)]
pub features: Option<Vec<String>>,
/// Rustc platform triple
#[clap(long)]
pub target: Option<String>,
/// Extra arguments passed to cargo build
#[clap(last = true)]
pub cargo_args: Vec<String>,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)]
@@ -129,4 +137,12 @@ pub struct ConfigOptsBundle {
/// Space separated list of features to activate
#[clap(long)]
pub features: Option<Vec<String>>,
/// Rustc platform triple
#[clap(long)]
pub target: Option<String>,
/// Extra arguments passed to cargo build
#[clap(last = true)]
pub cargo_args: Vec<String>,
}
+6
View File
@@ -34,6 +34,12 @@ impl Serve {
// Subdirectories don't work with the server
crate_config.dioxus_config.web.app.base_path = None;
if let Some(target) = self.serve.target {
crate_config.set_target(target);
}
crate_config.set_cargo_args(self.serve.cargo_args);
let platform = self
.serve
.platform
+16
View File
@@ -211,6 +211,8 @@ pub struct CrateConfig {
pub verbose: bool,
pub custom_profile: Option<String>,
pub features: Option<Vec<String>>,
pub target: Option<String>,
pub cargo_args: Vec<String>,
}
#[derive(Debug, Clone)]
@@ -278,6 +280,8 @@ impl CrateConfig {
let verbose = false;
let custom_profile = None;
let features = None;
let target = None;
let cargo_args = vec![];
Ok(Self {
out_dir,
@@ -294,6 +298,8 @@ impl CrateConfig {
custom_profile,
features,
verbose,
target,
cargo_args,
})
}
@@ -331,6 +337,16 @@ impl CrateConfig {
self.features = Some(features);
self
}
pub fn set_target(&mut self, target: String) -> &mut Self {
self.target = Some(target);
self
}
pub fn set_cargo_args(&mut self, cargo_args: Vec<String>) -> &mut Self {
self.cargo_args = cargo_args;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+16 -13
View File
@@ -42,34 +42,36 @@ async fn main() -> anyhow::Result<()> {
set_up_logging();
let bin = get_bin(args.bin)?;
let bin = get_bin(args.bin);
let _dioxus_config = DioxusConfig::load(Some(bin.clone()))
if let Ok(bin) = &bin {
let _dioxus_config = DioxusConfig::load(Some(bin.clone()))
.map_err(|e| anyhow!("Failed to load Dioxus config because: {e}"))?
.unwrap_or_else(|| {
log::warn!("You appear to be creating a Dioxus project from scratch; we will use the default config");
DioxusConfig::default()
});
#[cfg(feature = "plugin")]
PluginManager::init(_dioxus_config.plugin)
.map_err(|e| anyhow!("🚫 Plugin system initialization failed: {e}"))?;
#[cfg(feature = "plugin")]
PluginManager::init(_dioxus_config.plugin)
.map_err(|e| anyhow!("🚫 Plugin system initialization failed: {e}"))?;
}
match args.action {
Translate(opts) => opts
.translate()
.map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)),
Build(opts) => opts
.build(Some(bin.clone()))
Build(opts) if bin.is_ok() => opts
.build(Some(bin.unwrap().clone()))
.map_err(|e| anyhow!("🚫 Building project failed: {}", e)),
Clean(opts) => opts
.clean(Some(bin.clone()))
Clean(opts) if bin.is_ok() => opts
.clean(Some(bin.unwrap().clone()))
.map_err(|e| anyhow!("🚫 Cleaning project failed: {}", e)),
Serve(opts) => opts
.serve(Some(bin.clone()))
Serve(opts) if bin.is_ok() => opts
.serve(Some(bin.unwrap().clone()))
.await
.map_err(|e| anyhow!("🚫 Serving project failed: {}", e)),
@@ -81,8 +83,8 @@ async fn main() -> anyhow::Result<()> {
.config()
.map_err(|e| anyhow!("🚫 Configuring new project failed: {}", e)),
Bundle(opts) => opts
.bundle(Some(bin.clone()))
Bundle(opts) if bin.is_ok() => opts
.bundle(Some(bin.unwrap().clone()))
.map_err(|e| anyhow!("🚫 Bundling project failed: {}", e)),
#[cfg(feature = "plugin")]
@@ -107,5 +109,6 @@ async fn main() -> anyhow::Result<()> {
Ok(())
}
_ => Err(anyhow::anyhow!(bin.unwrap_err())),
}
}
+5 -1
View File
@@ -154,7 +154,11 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
println!("Connected to hot reloading 🚀");
}
Err(err) => {
if err.kind() != std::io::ErrorKind::WouldBlock {
let error_string = err.to_string();
// Filter out any error messages about a operation that may block and an error message that triggers on some operating systems that says "Waiting for a process to open the other end of the pipe" without WouldBlock being set
let display_error = err.kind() != std::io::ErrorKind::WouldBlock
&& !error_string.contains("Waiting for a process");
if display_error {
println!("Error connecting to hot reloading: {} (Hot reloading is a feature of the dioxus-cli. If you are not using the CLI, this error can be ignored)", err);
}
}
+9 -1
View File
@@ -11,6 +11,7 @@ use axum::{
body::{Full, HttpBody},
extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
http::{
self,
header::{HeaderName, HeaderValue},
Method, Response, StatusCode,
},
@@ -262,7 +263,7 @@ async fn setup_router(
.override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
.and_then(
move |response: Response<ServeFileSystemResponseBody>| async move {
let response = if file_service_config
let mut response = if file_service_config
.dioxus_config
.web
.watcher
@@ -290,6 +291,13 @@ async fn setup_router(
} else {
response.map(|body| body.boxed())
};
let headers = response.headers_mut();
headers.insert(
http::header::CACHE_CONTROL,
HeaderValue::from_static("no-cache"),
);
headers.insert(http::header::PRAGMA, HeaderValue::from_static("no-cache"));
headers.insert(http::header::EXPIRES, HeaderValue::from_static("0"));
Ok(response)
},
)
@@ -58,8 +58,11 @@ impl ToTokens for ComponentDeserializerOutput {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let comp_fn = &self.comp_fn;
let props_struct = &self.props_struct;
let fn_ident = &comp_fn.sig.ident;
let doc = format!("Properties for the [`{fn_ident}`] component.");
tokens.append_all(quote! {
#[doc = #doc]
#props_struct
#[allow(non_snake_case)]
#comp_fn
+62 -58
View File
@@ -13,62 +13,50 @@ use crate::{
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct ElementId(pub usize);
pub(crate) struct ElementRef {
/// An Element that can be bubbled to's unique identifier.
///
/// `BubbleId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is
/// unmounted, then the `BubbleId` will be reused for a new component.
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct VNodeId(pub usize);
#[derive(Debug, Clone, Copy)]
pub struct ElementRef {
// the pathway of the real element inside the template
pub path: ElementPath,
pub(crate) path: ElementPath,
// The actual template
pub template: Option<NonNull<VNode<'static>>>,
pub(crate) template: VNodeId,
// The scope the element belongs to
pub scope: ScopeId,
pub(crate) scope: ScopeId,
}
#[derive(Clone, Copy, Debug)]
pub enum ElementPath {
Deep(&'static [u8]),
Root(usize),
}
impl ElementRef {
pub(crate) fn none() -> Self {
Self {
template: None,
path: ElementPath::Root(0),
scope: ScopeId::ROOT,
}
}
pub struct ElementPath {
pub(crate) path: &'static [u8],
}
impl VirtualDom {
pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId {
self.next_reference(template, ElementPath::Deep(path))
pub(crate) fn next_element(&mut self) -> ElementId {
ElementId(self.elements.insert(None))
}
pub(crate) fn next_root(&mut self, template: &VNode, path: usize) -> ElementId {
self.next_reference(template, ElementPath::Root(path))
}
pub(crate) fn next_vnode_ref(&mut self, vnode: &VNode) -> VNodeId {
let new_id = VNodeId(self.element_refs.insert(Some(unsafe {
std::mem::transmute::<NonNull<VNode>, _>(vnode.into())
})));
pub(crate) fn next_null(&mut self) -> ElementId {
let entry = self.elements.vacant_entry();
let id = entry.key();
// Set this id to be dropped when the scope is rerun
if let Some(scope) = self.runtime.current_scope_id() {
self.scopes[scope.0]
.element_refs_to_drop
.borrow_mut()
.push(new_id);
}
entry.insert(ElementRef::none());
ElementId(id)
}
fn next_reference(&mut self, template: &VNode, path: ElementPath) -> ElementId {
let entry = self.elements.vacant_entry();
let id = entry.key();
let scope = self.runtime.current_scope_id().unwrap_or(ScopeId::ROOT);
entry.insert(ElementRef {
// We know this is non-null because it comes from a reference
template: Some(unsafe { NonNull::new_unchecked(template as *const _ as *mut _) }),
path,
scope,
});
ElementId(id)
new_id
}
pub(crate) fn reclaim(&mut self, el: ElementId) {
@@ -76,7 +64,7 @@ impl VirtualDom {
.unwrap_or_else(|| panic!("cannot reclaim {:?}", el));
}
pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option<ElementRef> {
pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option<()> {
if el.0 == 0 {
panic!(
"Cannot reclaim the root element - {:#?}",
@@ -84,12 +72,12 @@ impl VirtualDom {
);
}
self.elements.try_remove(el.0)
self.elements.try_remove(el.0).map(|_| ())
}
pub(crate) fn update_template(&mut self, el: ElementId, node: &VNode) {
let node: *const VNode = node as *const _;
self.elements[el.0].template = unsafe { std::mem::transmute(node) };
pub(crate) fn set_template(&mut self, id: VNodeId, vnode: &VNode) {
self.element_refs[id.0] =
Some(unsafe { std::mem::transmute::<NonNull<VNode>, _>(vnode.into()) });
}
// Drop a scope and all its children
@@ -101,6 +89,15 @@ impl VirtualDom {
id,
});
// Remove all VNode ids from the scope
for id in self.scopes[id.0]
.element_refs_to_drop
.borrow_mut()
.drain(..)
{
self.element_refs.try_remove(id.0);
}
self.ensure_drop_safety(id);
if recursive {
@@ -145,14 +142,25 @@ impl VirtualDom {
}
/// Descend through the tree, removing any borrowed props and listeners
pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) {
pub(crate) fn ensure_drop_safety(&mut self, scope_id: ScopeId) {
let scope = &self.scopes[scope_id.0];
{
// Drop all element refs that could be invalidated when the component was rerun
let mut element_refs = self.scopes[scope_id.0].element_refs_to_drop.borrow_mut();
let element_refs_slab = &mut self.element_refs;
for element_ref in element_refs.drain(..) {
if let Some(element_ref) = element_refs_slab.get_mut(element_ref.0) {
*element_ref = None;
}
}
}
// make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we
// run the hooks (which hold an &mut Reference)
// recursively call ensure_drop_safety on all children
let mut props = scope.borrowed_props.borrow_mut();
props.drain(..).for_each(|comp| {
let props = { scope.borrowed_props.borrow_mut().clone() };
for comp in props {
let comp = unsafe { &*comp };
match comp.scope.get() {
Some(child) if child != scope_id => self.ensure_drop_safety(child),
@@ -161,7 +169,9 @@ impl VirtualDom {
if let Ok(mut props) = comp.props.try_borrow_mut() {
*props = None;
}
});
}
let scope = &self.scopes[scope_id.0];
scope.borrowed_props.borrow_mut().clear();
// Now that all the references are gone, we can safely drop our own references in our listeners.
let mut listeners = scope.attributes_to_drop_before_render.borrow_mut();
@@ -176,18 +186,12 @@ impl VirtualDom {
impl ElementPath {
pub(crate) fn is_decendant(&self, small: &&[u8]) -> bool {
match *self {
ElementPath::Deep(big) => small.len() <= big.len() && *small == &big[..small.len()],
ElementPath::Root(r) => small.len() == 1 && small[0] == r as u8,
}
small.len() <= self.path.len() && *small == &self.path[..small.len()]
}
}
impl PartialEq<&[u8]> for ElementPath {
fn eq(&self, other: &&[u8]) -> bool {
match *self {
ElementPath::Deep(deep) => deep.eq(*other),
ElementPath::Root(r) => other.len() == 1 && other[0] == r as u8,
}
self.path.eq(*other)
}
}
+80 -45
View File
@@ -1,5 +1,7 @@
use crate::any_props::AnyProps;
use crate::innerlude::{BorrowedAttributeValue, VComponent, VPlaceholder, VText};
use crate::innerlude::{
BorrowedAttributeValue, ElementPath, ElementRef, VComponent, VPlaceholder, VText,
};
use crate::mutations::Mutation;
use crate::mutations::Mutation::*;
use crate::nodes::VNode;
@@ -94,6 +96,9 @@ impl<'b> VirtualDom {
nodes_mut.resize(len, ElementId::default());
};
// Set this node id
node.stable_id.set(Some(self.next_vnode_ref(node)));
// The best renderers will have templates prehydrated and registered
// Just in case, let's create the template using instructions anyways
self.register_template(node.template.get());
@@ -181,15 +186,30 @@ impl<'b> VirtualDom {
use DynamicNode::*;
match &template.dynamic_nodes[idx] {
node @ Component { .. } | node @ Fragment(_) => {
self.create_dynamic_node(template, node, idx)
let template_ref = ElementRef {
path: ElementPath {
path: template.template.get().node_paths[idx],
},
template: template.stable_id().unwrap(),
scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)),
};
self.create_dynamic_node(template_ref, node)
}
Placeholder(VPlaceholder { id }) => {
let id = self.set_slot(template, id, idx);
Placeholder(VPlaceholder { id, parent }) => {
let template_ref = ElementRef {
path: ElementPath {
path: template.template.get().node_paths[idx],
},
template: template.stable_id().unwrap(),
scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)),
};
parent.set(Some(template_ref));
let id = self.set_slot(id);
self.mutations.push(CreatePlaceholder { id });
1
}
Text(VText { id, value }) => {
let id = self.set_slot(template, id, idx);
let id = self.set_slot(id);
self.create_static_text(value, id);
1
}
@@ -205,7 +225,7 @@ impl<'b> VirtualDom {
});
}
/// We write all the descndent data for this element
/// We write all the descendent data for this element
///
/// Elements can contain other nodes - and those nodes can be dynamic or static
///
@@ -265,7 +285,14 @@ impl<'b> VirtualDom {
.map(|sorted_index| dynamic_nodes[sorted_index].0);
for idx in reversed_iter {
let m = self.create_dynamic_node(template, &template.dynamic_nodes[idx], idx);
let boundary_ref = ElementRef {
path: ElementPath {
path: template.template.get().node_paths[idx],
},
template: template.stable_id().unwrap(),
scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)),
};
let m = self.create_dynamic_node(boundary_ref, &template.dynamic_nodes[idx]);
if m > 0 {
// The path is one shorter because the top node is the root
let path = &template.template.get().node_paths[idx][1..];
@@ -279,15 +306,15 @@ impl<'b> VirtualDom {
attrs: &mut Peekable<impl Iterator<Item = (usize, &'static [u8])>>,
root_idx: u8,
root: ElementId,
node: &VNode,
node: &'b VNode<'b>,
) {
while let Some((mut attr_id, path)) =
attrs.next_if(|(_, p)| p.first().copied() == Some(root_idx))
{
let id = self.assign_static_node_as_dynamic(path, root, node, attr_id);
let id = self.assign_static_node_as_dynamic(path, root);
loop {
self.write_attribute(&node.dynamic_attrs[attr_id], id);
self.write_attribute(node, attr_id, &node.dynamic_attrs[attr_id], id);
// Only push the dynamic attributes forward if they match the current path (same element)
match attrs.next_if(|(_, p)| *p == path) {
@@ -298,7 +325,13 @@ impl<'b> VirtualDom {
}
}
fn write_attribute(&mut self, attribute: &'b crate::Attribute<'b>, id: ElementId) {
fn write_attribute(
&mut self,
template: &'b VNode<'b>,
idx: usize,
attribute: &'b crate::Attribute<'b>,
id: ElementId,
) {
// Make sure we set the attribute's associated id
attribute.mounted_element.set(id);
@@ -307,6 +340,13 @@ impl<'b> VirtualDom {
match &attribute.value {
AttributeValue::Listener(_) => {
let path = &template.template.get().attr_paths[idx];
let element_ref = ElementRef {
path: ElementPath { path },
template: template.stable_id().unwrap(),
scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)),
};
self.elements[id.0] = Some(element_ref);
self.mutations.push(NewEventListener {
// all listeners start with "on"
name: &unbounded_name[2..],
@@ -330,7 +370,7 @@ impl<'b> VirtualDom {
fn load_template_root(&mut self, template: &VNode, root_idx: usize) -> ElementId {
// Get an ID for this root since it's a real root
let this_id = self.next_root(template, root_idx);
let this_id = self.next_element();
template.root_ids.borrow_mut()[root_idx] = this_id;
self.mutations.push(LoadTemplate {
@@ -353,8 +393,6 @@ impl<'b> VirtualDom {
&mut self,
path: &'static [u8],
this_id: ElementId,
template: &VNode,
attr_id: usize,
) -> ElementId {
if path.len() == 1 {
return this_id;
@@ -362,7 +400,7 @@ impl<'b> VirtualDom {
// if attribute is on a root node, then we've already created the element
// Else, it's deep in the template and we should create a new id for it
let id = self.next_element(template, template.template.get().attr_paths[attr_id]);
let id = self.next_element();
self.mutations.push(Mutation::AssignId {
path: &path[1..],
@@ -405,6 +443,7 @@ impl<'b> VirtualDom {
#[allow(unused_mut)]
pub(crate) fn register_template(&mut self, mut template: Template<'static>) {
let (path, byte_index) = template.name.rsplit_once(':').unwrap();
let byte_index = byte_index.parse::<usize>().unwrap();
// First, check if we've already seen this template
if self
@@ -439,27 +478,21 @@ impl<'b> VirtualDom {
pub(crate) fn create_dynamic_node(
&mut self,
template: &'b VNode<'b>,
parent: ElementRef,
node: &'b DynamicNode<'b>,
idx: usize,
) -> usize {
use DynamicNode::*;
match node {
Text(text) => self.create_dynamic_text(template, text, idx),
Placeholder(place) => self.create_placeholder(place, template, idx),
Component(component) => self.create_component_node(template, component),
Fragment(frag) => frag.iter().map(|child| self.create(child)).sum(),
Text(text) => self.create_dynamic_text(parent, text),
Placeholder(place) => self.create_placeholder(place, parent),
Component(component) => self.create_component_node(Some(parent), component),
Fragment(frag) => self.create_children(*frag, Some(parent)),
}
}
fn create_dynamic_text(
&mut self,
template: &'b VNode<'b>,
text: &'b VText<'b>,
idx: usize,
) -> usize {
fn create_dynamic_text(&mut self, parent: ElementRef, text: &'b VText<'b>) -> usize {
// Allocate a dynamic element reference for this text node
let new_id = self.next_element(template, template.template.get().node_paths[idx]);
let new_id = self.next_element();
// Make sure the text node is assigned to the correct element
text.id.set(Some(new_id));
@@ -470,7 +503,7 @@ impl<'b> VirtualDom {
// Add the mutation to the list
self.mutations.push(HydrateText {
id: new_id,
path: &template.template.get().node_paths[idx][1..],
path: &parent.path.path[1..],
value,
});
@@ -481,18 +514,20 @@ impl<'b> VirtualDom {
pub(crate) fn create_placeholder(
&mut self,
placeholder: &VPlaceholder,
template: &'b VNode<'b>,
idx: usize,
parent: ElementRef,
) -> usize {
// Allocate a dynamic element reference for this text node
let id = self.next_element(template, template.template.get().node_paths[idx]);
let id = self.next_element();
// Make sure the text node is assigned to the correct element
placeholder.id.set(Some(id));
// Assign the placeholder's parent
placeholder.parent.set(Some(parent));
// Assign the ID to the existing node in the template
self.mutations.push(AssignId {
path: &template.template.get().node_paths[idx][1..],
path: &parent.path.path[1..],
id,
});
@@ -502,7 +537,7 @@ impl<'b> VirtualDom {
pub(super) fn create_component_node(
&mut self,
template: &'b VNode<'b>,
parent: Option<ElementRef>,
component: &'b VComponent<'b>,
) -> usize {
use RenderReturn::*;
@@ -514,8 +549,11 @@ impl<'b> VirtualDom {
match unsafe { self.run_scope(scope).extend_lifetime_ref() } {
// Create the component's root element
Ready(t) => self.create_scope(scope, t),
Aborted(t) => self.mount_aborted(template, t),
Ready(t) => {
self.assign_boundary_ref(parent, t);
self.create_scope(scope, t)
}
Aborted(t) => self.mount_aborted(t, parent),
}
}
@@ -531,20 +569,17 @@ impl<'b> VirtualDom {
.unwrap_or_else(|| component.scope.get().unwrap())
}
fn mount_aborted(&mut self, parent: &'b VNode<'b>, placeholder: &VPlaceholder) -> usize {
let id = self.next_element(parent, &[]);
fn mount_aborted(&mut self, placeholder: &VPlaceholder, parent: Option<ElementRef>) -> usize {
let id = self.next_element();
self.mutations.push(Mutation::CreatePlaceholder { id });
placeholder.id.set(Some(id));
placeholder.parent.set(parent);
1
}
fn set_slot(
&mut self,
template: &'b VNode<'b>,
slot: &'b Cell<Option<ElementId>>,
id: usize,
) -> ElementId {
let id = self.next_element(template, template.template.get().node_paths[id]);
fn set_slot(&mut self, slot: &'b Cell<Option<ElementId>>) -> ElementId {
let id = self.next_element();
slot.set(Some(id));
id
}
+149 -65
View File
@@ -1,7 +1,10 @@
use crate::{
any_props::AnyProps,
arena::ElementId,
innerlude::{BorrowedAttributeValue, DirtyScope, VComponent, VPlaceholder, VText},
innerlude::{
BorrowedAttributeValue, DirtyScope, ElementPath, ElementRef, VComponent, VPlaceholder,
VText,
},
mutations::Mutation,
nodes::RenderReturn,
nodes::{DynamicNode, VNode},
@@ -39,19 +42,27 @@ impl<'b> VirtualDom {
(Ready(l), Aborted(p)) => self.diff_ok_to_err(l, p),
// Just move over the placeholder
(Aborted(l), Aborted(r)) => r.id.set(l.id.get()),
(Aborted(l), Aborted(r)) => {
r.id.set(l.id.get());
r.parent.set(l.parent.get())
}
// Placeholder becomes something
// We should also clear the error now
(Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]),
(Aborted(l), Ready(r)) => self.replace_placeholder(
l,
[r],
l.parent.get().expect("root node should not be none"),
),
};
}
self.runtime.scope_stack.borrow_mut().pop();
}
fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) {
let id = self.next_null();
let id = self.next_element();
p.id.set(Some(id));
p.parent.set(l.parent.get());
self.mutations.push(Mutation::CreatePlaceholder { id });
let pre_edits = self.mutations.edits.len();
@@ -81,12 +92,24 @@ impl<'b> VirtualDom {
if let Some(&template) = map.get(&byte_index) {
right_template.template.set(template);
if template != left_template.template.get() {
return self.replace(left_template, [right_template]);
let parent = left_template.parent.take();
return self.replace(left_template, [right_template], parent);
}
}
}
}
// Copy over the parent
{
right_template.parent.set(left_template.parent.get());
}
// Update the bubble id pointer
right_template.stable_id.set(left_template.stable_id.get());
if let Some(bubble_id) = right_template.stable_id.get() {
self.set_template(bubble_id, right_template);
}
// If the templates are the same, we don't need to do anything, nor do we want to
if templates_are_the_same(left_template, right_template) {
return;
@@ -105,12 +128,8 @@ impl<'b> VirtualDom {
.zip(right_template.dynamic_attrs.iter())
.for_each(|(left_attr, right_attr)| {
// Move over the ID from the old to the new
right_attr
.mounted_element
.set(left_attr.mounted_element.get());
// We want to make sure anything that gets pulled is valid
self.update_template(left_attr.mounted_element.get(), right_template);
let mounted_element = left_attr.mounted_element.get();
right_attr.mounted_element.set(mounted_element);
// If the attributes are different (or volatile), we need to update them
if left_attr.value != right_attr.value || left_attr.volatile {
@@ -123,8 +142,16 @@ impl<'b> VirtualDom {
.dynamic_nodes
.iter()
.zip(right_template.dynamic_nodes.iter())
.for_each(|(left_node, right_node)| {
self.diff_dynamic_node(left_node, right_node, right_template);
.enumerate()
.for_each(|(dyn_node_idx, (left_node, right_node))| {
let current_ref = ElementRef {
template: right_template.stable_id().unwrap(),
path: ElementPath {
path: left_template.template.get().node_paths[dyn_node_idx],
},
scope: self.runtime.scope_stack.borrow().last().copied().unwrap(),
};
self.diff_dynamic_node(left_node, right_node, current_ref);
});
// Make sure the roots get transferred over while we're here
@@ -135,30 +162,24 @@ impl<'b> VirtualDom {
right.push(element);
}
}
let root_ids = right_template.root_ids.borrow();
// Update the node refs
for i in 0..root_ids.len() {
if let Some(root_id) = root_ids.get(i) {
self.update_template(*root_id, right_template);
}
}
}
fn diff_dynamic_node(
&mut self,
left_node: &'b DynamicNode<'b>,
right_node: &'b DynamicNode<'b>,
node: &'b VNode<'b>,
parent: ElementRef,
) {
match (left_node, right_node) {
(Text(left), Text(right)) => self.diff_vtext(left, right, node),
(Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right),
(Placeholder(left), Placeholder(right)) => right.id.set(left.id.get()),
(Component(left), Component(right)) => self.diff_vcomponent(left, right, node),
(Placeholder(left), Fragment(right)) => self.replace_placeholder(left, *right),
(Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right),
(Text(left), Text(right)) => self.diff_vtext(left, right),
(Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right, parent),
(Placeholder(left), Placeholder(right)) => {
right.id.set(left.id.get());
right.parent.set(left.parent.get());
},
(Component(left), Component(right)) => self.diff_vcomponent(left, right, Some(parent)),
(Placeholder(left), Fragment(right)) => self.replace_placeholder(left, *right, parent),
(Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right, parent),
_ => todo!("This is an usual custom case for dynamic nodes. We don't know how to handle it yet."),
};
}
@@ -179,7 +200,7 @@ impl<'b> VirtualDom {
&mut self,
left: &'b VComponent<'b>,
right: &'b VComponent<'b>,
right_template: &'b VNode<'b>,
parent: Option<ElementRef>,
) {
if std::ptr::eq(left, right) {
return;
@@ -187,7 +208,7 @@ impl<'b> VirtualDom {
// Replace components that have different render fns
if left.render_fn != right.render_fn {
return self.replace_vcomponent(right_template, right, left);
return self.replace_vcomponent(right, left, parent);
}
// Make sure the new vcomponent has the right scopeid associated to it
@@ -228,11 +249,11 @@ impl<'b> VirtualDom {
fn replace_vcomponent(
&mut self,
right_template: &'b VNode<'b>,
right: &'b VComponent<'b>,
left: &'b VComponent<'b>,
parent: Option<ElementRef>,
) {
let m = self.create_component_node(right_template, right);
let m = self.create_component_node(parent, right);
let pre_edits = self.mutations.edits.len();
@@ -287,11 +308,12 @@ impl<'b> VirtualDom {
/// }
/// ```
fn light_diff_templates(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) {
let parent = left.parent.take();
match matching_components(left, right) {
None => self.replace(left, [right]),
None => self.replace(left, [right], parent),
Some(components) => components
.into_iter()
.for_each(|(l, r)| self.diff_vcomponent(l, r, right)),
.for_each(|(l, r)| self.diff_vcomponent(l, r, parent)),
}
}
@@ -299,11 +321,8 @@ impl<'b> VirtualDom {
///
/// This just moves the ID of the old node over to the new node, and then sets the text of the new node if it's
/// different.
fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>, node: &'b VNode<'b>) {
let id = left
.id
.get()
.unwrap_or_else(|| self.next_element(node, &[0]));
fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>) {
let id = left.id.get().unwrap_or_else(|| self.next_element());
right.id.set(Some(id));
if left.value != right.value {
@@ -312,7 +331,12 @@ impl<'b> VirtualDom {
}
}
fn diff_non_empty_fragment(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) {
fn diff_non_empty_fragment(
&mut self,
old: &'b [VNode<'b>],
new: &'b [VNode<'b>],
parent: ElementRef,
) {
let new_is_keyed = new[0].key.is_some();
let old_is_keyed = old[0].key.is_some();
debug_assert!(
@@ -325,9 +349,9 @@ impl<'b> VirtualDom {
);
if new_is_keyed && old_is_keyed {
self.diff_keyed_children(old, new);
self.diff_keyed_children(old, new, parent);
} else {
self.diff_non_keyed_children(old, new);
self.diff_non_keyed_children(old, new, parent);
}
}
@@ -339,7 +363,12 @@ impl<'b> VirtualDom {
// [... parent]
//
// the change list stack is in the same state when this function returns.
fn diff_non_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) {
fn diff_non_keyed_children(
&mut self,
old: &'b [VNode<'b>],
new: &'b [VNode<'b>],
parent: ElementRef,
) {
use std::cmp::Ordering;
// Handled these cases in `diff_children` before calling this function.
@@ -348,7 +377,9 @@ impl<'b> VirtualDom {
match old.len().cmp(&new.len()) {
Ordering::Greater => self.remove_nodes(&old[new.len()..]),
Ordering::Less => self.create_and_insert_after(&new[old.len()..], old.last().unwrap()),
Ordering::Less => {
self.create_and_insert_after(&new[old.len()..], old.last().unwrap(), parent)
}
Ordering::Equal => {}
}
@@ -373,7 +404,12 @@ impl<'b> VirtualDom {
// https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739
//
// The stack is empty upon entry.
fn diff_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) {
fn diff_keyed_children(
&mut self,
old: &'b [VNode<'b>],
new: &'b [VNode<'b>],
parent: ElementRef,
) {
if cfg!(debug_assertions) {
let mut keys = rustc_hash::FxHashSet::default();
let mut assert_unique_keys = |children: &'b [VNode<'b>]| {
@@ -401,7 +437,7 @@ impl<'b> VirtualDom {
//
// `shared_prefix_count` is the count of how many nodes at the start of
// `new` and `old` share the same keys.
let (left_offset, right_offset) = match self.diff_keyed_ends(old, new) {
let (left_offset, right_offset) = match self.diff_keyed_ends(old, new, parent) {
Some(count) => count,
None => return,
};
@@ -427,18 +463,18 @@ impl<'b> VirtualDom {
if left_offset == 0 {
// insert at the beginning of the old list
let foothold = &old[old.len() - right_offset];
self.create_and_insert_before(new_middle, foothold);
self.create_and_insert_before(new_middle, foothold, parent);
} else if right_offset == 0 {
// insert at the end the old list
let foothold = old.last().unwrap();
self.create_and_insert_after(new_middle, foothold);
self.create_and_insert_after(new_middle, foothold, parent);
} else {
// inserting in the middle
let foothold = &old[left_offset - 1];
self.create_and_insert_after(new_middle, foothold);
self.create_and_insert_after(new_middle, foothold, parent);
}
} else {
self.diff_keyed_middle(old_middle, new_middle);
self.diff_keyed_middle(old_middle, new_middle, parent);
}
}
@@ -451,6 +487,7 @@ impl<'b> VirtualDom {
&mut self,
old: &'b [VNode<'b>],
new: &'b [VNode<'b>],
parent: ElementRef,
) -> Option<(usize, usize)> {
let mut left_offset = 0;
@@ -466,7 +503,7 @@ impl<'b> VirtualDom {
// If that was all of the old children, then create and append the remaining
// new children and we're finished.
if left_offset == old.len() {
self.create_and_insert_after(&new[left_offset..], old.last().unwrap());
self.create_and_insert_after(&new[left_offset..], old.last().unwrap(), parent);
return None;
}
@@ -505,7 +542,12 @@ impl<'b> VirtualDom {
//
// Upon exit from this function, it will be restored to that same self.
#[allow(clippy::too_many_lines)]
fn diff_keyed_middle(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) {
fn diff_keyed_middle(
&mut self,
old: &'b [VNode<'b>],
new: &'b [VNode<'b>],
parent: ElementRef,
) {
/*
1. Map the old keys into a numerical ordering based on indices.
2. Create a map of old key to its index
@@ -562,7 +604,7 @@ impl<'b> VirtualDom {
if shared_keys.is_empty() {
if old.first().is_some() {
self.remove_nodes(&old[1..]);
self.replace(&old[0], new);
self.replace(&old[0], new, Some(parent));
} else {
// I think this is wrong - why are we appending?
// only valid of the if there are no trailing elements
@@ -739,20 +781,38 @@ impl<'b> VirtualDom {
.sum()
}
fn create_children(&mut self, nodes: impl IntoIterator<Item = &'b VNode<'b>>) -> usize {
pub(crate) fn create_children(
&mut self,
nodes: impl IntoIterator<Item = &'b VNode<'b>>,
parent: Option<ElementRef>,
) -> usize {
nodes
.into_iter()
.fold(0, |acc, child| acc + self.create(child))
.map(|child| {
self.assign_boundary_ref(parent, child);
self.create(child)
})
.sum()
}
fn create_and_insert_before(&mut self, new: &'b [VNode<'b>], before: &'b VNode<'b>) {
let m = self.create_children(new);
fn create_and_insert_before(
&mut self,
new: &'b [VNode<'b>],
before: &'b VNode<'b>,
parent: ElementRef,
) {
let m = self.create_children(new, Some(parent));
let id = self.find_first_element(before);
self.mutations.push(Mutation::InsertBefore { id, m })
}
fn create_and_insert_after(&mut self, new: &'b [VNode<'b>], after: &'b VNode<'b>) {
let m = self.create_children(new);
fn create_and_insert_after(
&mut self,
new: &'b [VNode<'b>],
after: &'b VNode<'b>,
parent: ElementRef,
) {
let m = self.create_children(new, Some(parent));
let id = self.find_last_element(after);
self.mutations.push(Mutation::InsertAfter { id, m })
}
@@ -762,15 +822,21 @@ impl<'b> VirtualDom {
&mut self,
l: &'b VPlaceholder,
r: impl IntoIterator<Item = &'b VNode<'b>>,
parent: ElementRef,
) {
let m = self.create_children(r);
let m = self.create_children(r, Some(parent));
let id = l.id.get().unwrap();
self.mutations.push(Mutation::ReplaceWith { id, m });
self.reclaim(id);
}
fn replace(&mut self, left: &'b VNode<'b>, right: impl IntoIterator<Item = &'b VNode<'b>>) {
let m = self.create_children(right);
fn replace(
&mut self,
left: &'b VNode<'b>,
right: impl IntoIterator<Item = &'b VNode<'b>>,
parent: Option<ElementRef>,
) {
let m = self.create_children(right, parent);
let pre_edits = self.mutations.edits.len();
@@ -789,11 +855,12 @@ impl<'b> VirtualDom {
};
}
fn node_to_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b VPlaceholder) {
fn node_to_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b VPlaceholder, parent: ElementRef) {
// Create the placeholder first, ensuring we get a dedicated ID for the placeholder
let placeholder = self.next_element(&l[0], &[]);
let placeholder = self.next_element();
r.id.set(Some(placeholder));
r.parent.set(Some(parent));
self.mutations
.push(Mutation::CreatePlaceholder { id: placeholder });
@@ -831,6 +898,16 @@ impl<'b> VirtualDom {
// Clean up the roots, assuming we need to generate mutations for these
// This is done last in order to preserve Node ID reclaim order (reclaim in reverse order of claim)
self.reclaim_roots(node, gen_muts);
// Clean up the vnode id
self.reclaim_vnode_id(node);
}
fn reclaim_vnode_id(&mut self, node: &'b VNode<'b>) {
// Clean up the vnode id
if let Some(id) = node.stable_id() {
self.element_refs.remove(id.0);
}
}
fn reclaim_roots(&mut self, node: &VNode, gen_muts: bool) {
@@ -989,6 +1066,13 @@ impl<'b> VirtualDom {
}
}
}
pub(crate) fn assign_boundary_ref(&mut self, parent: Option<ElementRef>, child: &'b VNode<'b>) {
if let Some(parent) = parent {
// assign the parent of the child
child.parent.set(Some(parent));
}
}
}
/// Are the templates the same?
+2 -1
View File
@@ -30,7 +30,8 @@ pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element {
let children = cx.props.0.as_ref()?;
Some(VNode {
key: children.key,
parent: children.parent,
parent: children.parent.clone(),
stable_id: children.stable_id.clone(),
template: children.template.clone(),
root_ids: children.root_ids.clone(),
dynamic_nodes: children.dynamic_nodes,
+1 -1
View File
@@ -18,7 +18,7 @@ use crate::{innerlude::VNode, ScopeState};
/// A concrete type provider for closures that build [`VNode`] structures.
///
/// This struct wraps lazy structs that build [`VNode`] trees Normally, we cannot perform a blanket implementation over
/// This struct wraps lazy structs that build [`VNode`] trees. Normally, we cannot perform a blanket implementation over
/// closures, but if we wrap the closure in a concrete type, we can use it for different branches in matching.
///
///
+36 -4
View File
@@ -1,3 +1,4 @@
use crate::innerlude::{ElementRef, VNodeId};
use crate::{
any_props::AnyProps, arena::ElementId, Element, Event, LazyNodes, ScopeId, ScopeState,
};
@@ -47,7 +48,10 @@ pub struct VNode<'a> {
pub key: Option<&'a str>,
/// When rendered, this template will be linked to its parent manually
pub parent: Option<ElementId>,
pub(crate) parent: Cell<Option<ElementRef>>,
/// The bubble id assigned to the child that we need to update and drop when diffing happens
pub(crate) stable_id: Cell<Option<VNodeId>>,
/// The static nodes and static descriptor of the template
pub template: Cell<Template<'static>>,
@@ -68,7 +72,8 @@ impl<'a> VNode<'a> {
pub fn empty(cx: &'a ScopeState) -> Element<'a> {
Some(VNode {
key: None,
parent: None,
parent: Default::default(),
stable_id: Default::default(),
root_ids: RefCell::new(bumpalo::collections::Vec::new_in(cx.bump())),
dynamic_nodes: &[],
dynamic_attrs: &[],
@@ -81,6 +86,30 @@ impl<'a> VNode<'a> {
})
}
/// Create a new VNode
pub fn new(
key: Option<&'a str>,
template: Template<'static>,
root_ids: bumpalo::collections::Vec<'a, ElementId>,
dynamic_nodes: &'a [DynamicNode<'a>],
dynamic_attrs: &'a [Attribute<'a>],
) -> Self {
Self {
key,
parent: Cell::new(None),
stable_id: Cell::new(None),
template: Cell::new(template),
root_ids: RefCell::new(root_ids),
dynamic_nodes,
dynamic_attrs,
}
}
/// Get the stable id of this node used for bubbling events
pub(crate) fn stable_id(&self) -> Option<VNodeId> {
self.stable_id.get()
}
/// Load a dynamic root at the given index
///
/// Returns [`None`] if the root is actually a static node (Element/Text)
@@ -319,7 +348,7 @@ pub struct VComponent<'a> {
/// The function pointer of the component, known at compile time
///
/// It is possible that components get folded at comppile time, so these shouldn't be really used as a key
/// It is possible that components get folded at compile time, so these shouldn't be really used as a key
pub(crate) render_fn: *const (),
pub(crate) props: RefCell<Option<Box<dyn AnyProps<'a> + 'a>>>,
@@ -372,6 +401,8 @@ impl<'a> VText<'a> {
pub struct VPlaceholder {
/// The ID of this node in the real DOM
pub(crate) id: Cell<Option<ElementId>>,
/// The parent of this node
pub(crate) parent: Cell<Option<ElementRef>>,
}
impl VPlaceholder {
@@ -722,7 +753,8 @@ impl<'b> IntoDynNode<'b> for Arguments<'_> {
impl<'a> IntoDynNode<'a> for &'a VNode<'a> {
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
DynamicNode::Fragment(_cx.bump().alloc([VNode {
parent: self.parent,
parent: self.parent.clone(),
stable_id: self.stable_id.clone(),
template: self.template.clone(),
root_ids: self.root_ids.clone(),
key: self.key,
+11 -7
View File
@@ -19,7 +19,7 @@ pub struct TaskId(pub usize);
/// the task itself is the waker
pub(crate) struct LocalTask {
pub scope: ScopeId,
pub(super) task: RefCell<Pin<Box<dyn Future<Output = ()> + 'static>>>,
pub task: RefCell<Pin<Box<dyn Future<Output = ()> + 'static>>>,
pub waker: Waker,
}
@@ -48,11 +48,15 @@ impl Scheduler {
})),
};
entry.insert(task);
let mut cx = std::task::Context::from_waker(&task.waker);
self.sender
.unbounded_send(SchedulerMsg::TaskNotified(task_id))
.expect("Scheduler should exist");
if !task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() {
self.sender
.unbounded_send(SchedulerMsg::TaskNotified(task_id))
.expect("Scheduler should exist");
}
entry.insert(task);
task_id
}
@@ -60,8 +64,8 @@ impl Scheduler {
/// Drop the future with the given TaskId
///
/// This does not abort the task, so you'll want to wrap it in an aborthandle if that's important to you
pub fn remove(&self, id: TaskId) {
self.tasks.borrow_mut().try_remove(id.0);
pub fn remove(&self, id: TaskId) -> Option<LocalTask> {
self.tasks.borrow_mut().try_remove(id.0)
}
}
+1
View File
@@ -36,6 +36,7 @@ impl VirtualDom {
borrowed_props: Default::default(),
attributes_to_drop_before_render: Default::default(),
element_refs_to_drop: Default::default(),
}));
let context =
+1 -11
View File
@@ -230,17 +230,7 @@ impl ScopeContext {
/// This is good for tasks that need to be run after the component has been dropped.
pub fn spawn_forever(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
// The root scope will never be unmounted so we can just add the task at the top of the app
let id = self.tasks.spawn(ScopeId::ROOT, fut);
// wake up the scheduler if it is sleeping
self.tasks
.sender
.unbounded_send(SchedulerMsg::TaskNotified(id))
.expect("Scheduler should exist");
self.spawned_tasks.borrow_mut().insert(id);
id
self.tasks.spawn(ScopeId::ROOT, fut)
}
/// Informs the scheduler that this task is no longer needed and should be removed.
+3 -2
View File
@@ -3,7 +3,7 @@ use crate::{
any_props::VProps,
bump_frame::BumpFrame,
innerlude::ErrorBoundary,
innerlude::{DynamicNode, EventHandler, VComponent, VText},
innerlude::{DynamicNode, EventHandler, VComponent, VNodeId, VText},
lazynodes::LazyNodes,
nodes::{IntoAttributeValue, IntoDynNode, RenderReturn},
runtime::Runtime,
@@ -94,6 +94,7 @@ pub struct ScopeState {
pub(crate) hook_idx: Cell<usize>,
pub(crate) borrowed_props: RefCell<Vec<*const VComponent<'static>>>,
pub(crate) element_refs_to_drop: RefCell<Vec<VNodeId>>,
pub(crate) attributes_to_drop_before_render: RefCell<Vec<*const Attribute<'static>>>,
pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
@@ -466,7 +467,7 @@ impl<'src> ScopeState {
render_fn: component as *const (),
static_props: P::IS_STATIC,
props: RefCell::new(Some(extended)),
scope: Cell::new(None),
scope: Default::default(),
})
}
+82 -74
View File
@@ -4,19 +4,19 @@
use crate::{
any_props::VProps,
arena::{ElementId, ElementRef},
innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
arena::ElementId,
innerlude::{DirtyScope, ElementRef, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
mutations::Mutation,
nodes::RenderReturn,
nodes::{Template, TemplateId},
runtime::{Runtime, RuntimeGuard},
scopes::{ScopeId, ScopeState},
AttributeValue, Element, Event, Scope,
AttributeValue, Element, Event, Scope, VNode,
};
use futures_util::{pin_mut, StreamExt};
use rustc_hash::{FxHashMap, FxHashSet};
use slab::Slab;
use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, ptr::NonNull, rc::Rc};
/// A virtual node system that progresses user events and diffs UI trees.
///
@@ -186,7 +186,10 @@ pub struct VirtualDom {
pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template<'static>>>,
// Every element is actually a dual reference - one to the template and the other to the dynamic node in that template
pub(crate) elements: Slab<ElementRef>,
pub(crate) element_refs: Slab<Option<NonNull<VNode<'static>>>>,
// The element ids that are used in the renderer
pub(crate) elements: Slab<Option<ElementRef>>,
pub(crate) mutations: Mutations<'static>,
@@ -263,6 +266,7 @@ impl VirtualDom {
dirty_scopes: Default::default(),
templates: Default::default(),
elements: Default::default(),
element_refs: Default::default(),
mutations: Mutations::default(),
suspended_scopes: Default::default(),
};
@@ -276,7 +280,7 @@ impl VirtualDom {
root.provide_context(Rc::new(ErrorBoundary::new(ScopeId::ROOT)));
// the root element is always given element ID 0 since it's the container for the entire tree
dom.elements.insert(ElementRef::none());
dom.elements.insert(None);
dom
}
@@ -314,9 +318,9 @@ impl VirtualDom {
}
}
/// Call a listener inside the VirtualDom with data from outside the VirtualDom.
/// Call a listener inside the VirtualDom with data from outside the VirtualDom. **The ElementId passed in must be the id of an dynamic element, not a static node or a text node.**
///
/// This method will identify the appropriate element. The data must match up with the listener delcared. Note that
/// This method will identify the appropriate element. The data must match up with the listener declared. Note that
/// this method does not give any indication as to the success of the listener call. If the listener is not found,
/// nothing will happen.
///
@@ -353,7 +357,15 @@ impl VirtualDom {
| | | <-- no, broke early
| <-- no, broke early
*/
let mut parent_path = self.elements.get(element.0);
let parent_path = match self.elements.get(element.0) {
Some(Some(el)) => el,
_ => return,
};
let mut parent_node = self
.element_refs
.get(parent_path.template.0)
.cloned()
.map(|el| (*parent_path, el));
let mut listeners = vec![];
// We will clone this later. The data itself is wrapped in RC to be used in callbacks if required
@@ -365,36 +377,72 @@ impl VirtualDom {
// If the event bubbles, we traverse through the tree until we find the target element.
if bubbles {
// Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent.
while let Some(el_ref) = parent_path {
while let Some((path, el_ref)) = parent_node {
// safety: we maintain references of all vnodes in the element slab
if let Some(template) = el_ref.template {
let template = unsafe { template.as_ref() };
let node_template = template.template.get();
let target_path = el_ref.path;
let template = unsafe { el_ref.unwrap().as_ref() };
let node_template = template.template.get();
let target_path = path.path;
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
let this_path = node_template.attr_paths[idx];
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
let this_path = node_template.attr_paths[idx];
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
if attr.name.trim_start_matches("on") == name
&& target_path.is_decendant(&this_path)
{
listeners.push(&attr.value);
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
if attr.name.trim_start_matches("on") == name
&& target_path.is_decendant(&this_path)
{
listeners.push(&attr.value);
// Break if this is the exact target element.
// This means we won't call two listeners with the same name on the same element. This should be
// documented, or be rejected from the rsx! macro outright
if target_path == this_path {
break;
}
// Break if this is the exact target element.
// This means we won't call two listeners with the same name on the same element. This should be
// documented, or be rejected from the rsx! macro outright
if target_path == this_path {
break;
}
}
}
// Now that we've accumulated all the parent attributes for the target element, call them in reverse order
// We check the bubble state between each call to see if the event has been stopped from bubbling
for listener in listeners.drain(..).rev() {
if let AttributeValue::Listener(listener) = listener {
let origin = el_ref.scope;
// Now that we've accumulated all the parent attributes for the target element, call them in reverse order
// We check the bubble state between each call to see if the event has been stopped from bubbling
for listener in listeners.drain(..).rev() {
if let AttributeValue::Listener(listener) = listener {
let origin = path.scope;
self.runtime.scope_stack.borrow_mut().push(origin);
self.runtime.rendering.set(false);
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
cb(uievent.clone());
}
self.runtime.scope_stack.borrow_mut().pop();
self.runtime.rendering.set(true);
if !uievent.propagates.get() {
return;
}
}
}
parent_node = template.parent.get().and_then(|element_ref| {
self.element_refs
.get(element_ref.template.0)
.cloned()
.map(|el| (element_ref, el))
});
}
} else {
// Otherwise, we just call the listener on the target element
if let Some((path, el_ref)) = parent_node {
// safety: we maintain references of all vnodes in the element slab
let template = unsafe { el_ref.unwrap().as_ref() };
let node_template = template.template.get();
let target_path = path.path;
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
let this_path = node_template.attr_paths[idx];
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
// Only call the listener if this is the exact target element.
if attr.name.trim_start_matches("on") == name && target_path == this_path {
if let AttributeValue::Listener(listener) = &attr.value {
let origin = path.scope;
self.runtime.scope_stack.borrow_mut().push(origin);
self.runtime.rendering.set(false);
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
@@ -403,44 +451,7 @@ impl VirtualDom {
self.runtime.scope_stack.borrow_mut().pop();
self.runtime.rendering.set(true);
if !uievent.propagates.get() {
return;
}
}
}
parent_path = template.parent.and_then(|id| self.elements.get(id.0));
} else {
break;
}
}
} else {
// Otherwise, we just call the listener on the target element
if let Some(el_ref) = parent_path {
// safety: we maintain references of all vnodes in the element slab
if let Some(template) = el_ref.template {
let template = unsafe { template.as_ref() };
let node_template = template.template.get();
let target_path = el_ref.path;
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
let this_path = node_template.attr_paths[idx];
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
// Only call the listener if this is the exact target element.
if attr.name.trim_start_matches("on") == name && target_path == this_path {
if let AttributeValue::Listener(listener) = &attr.value {
let origin = el_ref.scope;
self.runtime.scope_stack.borrow_mut().push(origin);
self.runtime.rendering.set(false);
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
cb(uievent.clone());
}
self.runtime.scope_stack.borrow_mut().pop();
self.runtime.rendering.set(true);
break;
}
break;
}
}
}
@@ -563,7 +574,7 @@ impl VirtualDom {
// If an error occurs, we should try to render the default error component and context where the error occured
RenderReturn::Aborted(placeholder) => {
tracing::debug!("Ran into suspended or aborted scope during rebuild");
let id = self.next_null();
let id = self.next_element();
placeholder.id.set(Some(id));
self.mutations.push(Mutation::CreatePlaceholder { id });
}
@@ -595,15 +606,12 @@ impl VirtualDom {
/// The mutations will be thrown out, so it's best to use this method for things like SSR that have async content
pub async fn wait_for_suspense(&mut self) {
loop {
// println!("waiting for suspense {:?}", self.suspended_scopes);
if self.suspended_scopes.is_empty() {
return;
}
// println!("waiting for suspense");
self.wait_for_work().await;
// println!("Rendered immediately");
_ = self.render_immediate();
}
}
+69
View File
@@ -0,0 +1,69 @@
use dioxus::prelude::*;
use dioxus_core::ElementId;
use std::{rc::Rc, sync::Mutex};
static CLICKS: Mutex<usize> = Mutex::new(0);
#[test]
fn events_propagate() {
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
// Top-level click is registered
dom.handle_event("click", Rc::new(MouseData::default()), ElementId(1), true);
assert_eq!(*CLICKS.lock().unwrap(), 1);
// break reference....
for _ in 0..5 {
dom.mark_dirty(ScopeId(0));
_ = dom.render_immediate();
}
// Lower click is registered
dom.handle_event("click", Rc::new(MouseData::default()), ElementId(2), true);
assert_eq!(*CLICKS.lock().unwrap(), 3);
// break reference....
for _ in 0..5 {
dom.mark_dirty(ScopeId(0));
_ = dom.render_immediate();
}
// Stop propagation occurs
dom.handle_event("click", Rc::new(MouseData::default()), ElementId(2), true);
assert_eq!(*CLICKS.lock().unwrap(), 3);
}
fn app(cx: Scope) -> Element {
render! {
div {
onclick: move |_| {
println!("top clicked");
*CLICKS.lock().unwrap() += 1;
},
vec![
render! {
problematic_child {}
}
].into_iter()
}
}
}
fn problematic_child(cx: Scope) -> Element {
render! {
button {
onclick: move |evt| {
println!("bottom clicked");
let mut clicks = CLICKS.lock().unwrap();
if *clicks == 3 {
evt.stop_propagation();
} else {
*clicks += 1;
}
}
}
}
}
+29 -30
View File
@@ -2,7 +2,7 @@
use dioxus::prelude::Props;
use dioxus_core::*;
use std::{cell::Cell, collections::HashSet};
use std::{cfg, collections::HashSet};
fn random_ns() -> Option<&'static str> {
let namespace = rand::random::<u8>() % 2;
@@ -170,22 +170,23 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode {
let range = if depth > 5 { 1 } else { 4 };
match rand::random::<u8>() % range {
0 => DynamicNode::Placeholder(Default::default()),
1 => cx.make_node((0..(rand::random::<u8>() % 5)).map(|_| VNode {
key: None,
parent: Default::default(),
template: Cell::new(Template {
name: concat!(file!(), ":", line!(), ":", column!(), ":0"),
roots: &[TemplateNode::Dynamic { id: 0 }],
node_paths: &[&[0]],
attr_paths: &[],
}),
root_ids: bumpalo::collections::Vec::new_in(cx.bump()).into(),
dynamic_nodes: cx.bump().alloc([cx.component(
create_random_element,
DepthProps { depth, root: false },
"create_random_element",
)]),
dynamic_attrs: &[],
1 => cx.make_node((0..(rand::random::<u8>() % 5)).map(|_| {
VNode::new(
None,
Template {
name: concat!(file!(), ":", line!(), ":", column!(), ":0"),
roots: &[TemplateNode::Dynamic { id: 0 }],
node_paths: &[&[0]],
attr_paths: &[],
},
bumpalo::collections::Vec::new_in(cx.bump()),
cx.bump().alloc([cx.component(
create_random_element,
DepthProps { depth, root: false },
"create_random_element",
)]),
&[],
)
})),
2 => cx.component(
create_random_element,
@@ -271,13 +272,11 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
)
.into_boxed_str(),
));
// println!("{template:#?}");
let node = VNode {
key: None,
parent: None,
template: Cell::new(template),
root_ids: bumpalo::collections::Vec::new_in(cx.bump()).into(),
dynamic_nodes: {
let node = VNode::new(
None,
template,
bumpalo::collections::Vec::new_in(cx.bump()),
{
let dynamic_nodes: Vec<_> = dynamic_node_types
.iter()
.map(|ty| match ty {
@@ -291,12 +290,12 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
.collect();
cx.bump().alloc(dynamic_nodes)
},
dynamic_attrs: cx.bump().alloc(
cx.bump().alloc(
(0..template.attr_paths.len())
.map(|_| create_random_dynamic_attr(cx))
.collect::<Vec<_>>(),
),
};
);
Some(node)
}
_ => None,
@@ -306,10 +305,10 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
}
// test for panics when creating random nodes and templates
#[cfg(not(miri))]
#[test]
fn create() {
for _ in 0..1000 {
let repeat_count = if cfg!(miri) { 100 } else { 1000 };
for _ in 0..repeat_count {
let mut vdom =
VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true });
let _ = vdom.rebuild();
@@ -318,10 +317,10 @@ fn create() {
// test for panics when diffing random nodes
// This test will change the template every render which is not very realistic, but it helps stress the system
#[cfg(not(miri))]
#[test]
fn diff() {
for _ in 0..100000 {
let repeat_count = if cfg!(miri) { 100 } else { 1000 };
for _ in 0..repeat_count {
let mut vdom =
VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true });
let _ = vdom.rebuild();
+2 -2
View File
@@ -18,8 +18,8 @@ dioxus-hot-reload = { workspace = true, optional = true }
serde = "1.0.136"
serde_json = "1.0.79"
thiserror = { workspace = true }
wry = { version = "0.33.0", default-features = false, features = ["protocol", "file-drop", "tao"] }
tracing = { workspace = true }
wry = { version = "0.34.0", default-features = false, features = ["tao", "protocol", "file-drop"] }
futures-channel = { workspace = true }
tokio = { workspace = true, features = [
"sync",
@@ -42,7 +42,7 @@ crossbeam-channel = "0.5.8"
[target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies]
rfd = "0.11.3"
rfd = "0.12"
global-hotkey = { git = "https://github.com/tauri-apps/global-hotkey" }
[target.'cfg(target_os = "ios")'.dependencies]
+21 -18
View File
@@ -232,6 +232,12 @@ fn app(cx: Scope) -> Element {
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary));
received_events.modify(|x| *x + 1)
},
assert_eq!(
event.data.trigger_button(),
Some(dioxus_html::input_data::MouseButton::Primary),
);
recieved_events.modify(|x| *x + 1)
}
}
div {
id: "mouse_move_div",
@@ -240,7 +246,7 @@ fn app(cx: Scope) -> Element {
assert!(event.data.modifiers().is_empty());
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
received_events.modify(|x| *x + 1)
},
}
}
div {
id: "mouse_click_div",
@@ -250,11 +256,11 @@ fn app(cx: Scope) -> Element {
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
received_events.modify(|x| *x + 1)
},
}
}
div{
div {
id: "mouse_dblclick_div",
ondblclick: move |event| {
ondoubleclick: move |event| {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary));
@@ -263,7 +269,7 @@ fn app(cx: Scope) -> Element {
received_events.modify(|x| *x + 1)
}
}
div{
div {
id: "mouse_down_div",
onmousedown: move |event| {
println!("{:?}", event.data);
@@ -273,7 +279,7 @@ fn app(cx: Scope) -> Element {
received_events.modify(|x| *x + 1)
}
}
div{
div {
id: "mouse_up_div",
onmouseup: move |event| {
println!("{:?}", event.data);
@@ -283,21 +289,20 @@ fn app(cx: Scope) -> Element {
received_events.modify(|x| *x + 1)
}
}
div{
div {
id: "wheel_div",
width: "100px",
height: "100px",
background_color: "red",
onwheel: move |event| {
println!("{:?}", event.data);
let dioxus_html::geometry::WheelDelta::Pixels(delta)= event.data.delta()else{
panic!("Expected delta to be in pixels")
};
let dioxus_html::geometry::WheelDelta::Pixels(delta) = event.data.delta() else {
panic!("Expected delta to be in pixels") };
assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0));
received_events.modify(|x| *x + 1)
}
}
input{
input {
id: "key_down_div",
onkeydown: move |event| {
println!("{:?}", event.data);
@@ -306,11 +311,11 @@ fn app(cx: Scope) -> Element {
assert_eq!(event.data.code().to_string(), "KeyA");
assert_eq!(event.data.location, 0);
assert!(event.data.is_auto_repeating());
received_events.modify(|x| *x + 1)
}
}
input{
input {
id: "key_up_div",
onkeyup: move |event| {
println!("{:?}", event.data);
@@ -319,11 +324,10 @@ fn app(cx: Scope) -> Element {
assert_eq!(event.data.code().to_string(), "KeyA");
assert_eq!(event.data.location, 0);
assert!(!event.data.is_auto_repeating());
received_events.modify(|x| *x + 1)
}
}
input{
input {
id: "key_press_div",
onkeypress: move |event| {
println!("{:?}", event.data);
@@ -332,18 +336,17 @@ fn app(cx: Scope) -> Element {
assert_eq!(event.data.code().to_string(), "KeyA");
assert_eq!(event.data.location, 0);
assert!(!event.data.is_auto_repeating());
received_events.modify(|x| *x + 1)
}
}
input{
input {
id: "focus_in_div",
onfocusin: move |event| {
println!("{:?}", event.data);
received_events.modify(|x| *x + 1)
}
}
input{
input {
id: "focus_out_div",
onfocusout: move |event| {
println!("{:?}", event.data);
+10
View File
@@ -35,6 +35,7 @@ pub struct Config {
pub(crate) root_name: String,
pub(crate) background_color: Option<(u8, u8, u8, u8)>,
pub(crate) last_window_close_behaviour: WindowCloseBehaviour,
pub(crate) enable_default_menu_bar: bool,
}
type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>;
@@ -64,9 +65,18 @@ impl Config {
root_name: "main".to_string(),
background_color: None,
last_window_close_behaviour: WindowCloseBehaviour::LastWindowExitsApp,
enable_default_menu_bar: true,
}
}
/// Set whether the default menu bar should be enabled.
///
/// > Note: `enable` is `true` by default. To disable the default menu bar pass `false`.
pub fn with_default_menu_bar(mut self, enable: bool) -> Self {
self.enable_default_menu_bar = enable;
self
}
/// set the directory from which assets will be searched in release mode
pub fn with_resource_directory(mut self, path: impl Into<PathBuf>) -> Self {
self.resource_dir = Some(path.into());
+20
View File
@@ -1,7 +1,10 @@
use crate::create_new_window;
use crate::events::IpcMessage;
use crate::protocol::AssetFuture;
use crate::protocol::AssetHandlerRegistry;
use crate::query::QueryEngine;
use crate::shortcut::{HotKey, ShortcutId, ShortcutRegistry, ShortcutRegistryError};
use crate::AssetHandler;
use crate::Config;
use crate::WebviewHandler;
use dioxus_core::ScopeState;
@@ -114,6 +117,7 @@ pub struct DesktopService {
pub(crate) max_template_count: AtomicU16,
pub(crate) channel: RefCell<Channel>,
pub(crate) asset_handlers: AssetHandlerRegistry,
#[cfg(target_os = "ios")]
pub(crate) views: Rc<RefCell<Vec<*mut objc::runtime::Object>>>,
@@ -140,6 +144,7 @@ impl DesktopService {
event_handlers: WindowEventHandlers,
shortcut_manager: ShortcutRegistry,
edit_queue: EditQueue,
asset_handlers: AssetHandlerRegistry,
) -> Self {
Self {
webview: Rc::new(webview),
@@ -153,6 +158,7 @@ impl DesktopService {
templates: Default::default(),
max_template_count: Default::default(),
channel: Default::default(),
asset_handlers,
#[cfg(target_os = "ios")]
views: Default::default(),
}
@@ -303,6 +309,20 @@ impl DesktopService {
self.shortcut_manager.remove_all()
}
/// Provide a callback to handle asset loading yourself.
///
/// See [`use_asset_handle`](crate::use_asset_handle) for a convenient hook.
pub async fn register_asset_handler<F: AssetFuture>(&self, f: impl AssetHandler<F>) -> usize {
self.asset_handlers.register_handler(f).await
}
/// Removes an asset handler by its identifier.
///
/// Returns `None` if the handler did not exist.
pub async fn remove_asset_handler(&self, id: usize) -> Option<()> {
self.asset_handlers.remove_handler(id).await
}
/// Push an objc view to the window
#[cfg(target_os = "ios")]
pub fn push_view(&self, view: objc_id::ShareId<objc::runtime::Object>) {
+5 -1
View File
@@ -34,6 +34,7 @@ use element::DesktopElement;
use eval::init_eval;
use futures_util::{pin_mut, FutureExt};
use rustc_hash::FxHashMap;
pub use protocol::{use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse};
use shortcut::ShortcutRegistry;
pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
use std::cell::Cell;
@@ -48,6 +49,7 @@ use tao::{
event::{Event, StartCause, WindowEvent},
event_loop::ControlFlow,
};
pub use webview::build_default_menu_bar;
pub use wry;
pub use wry::application as tao;
use wry::application::event_loop::EventLoopBuilder;
@@ -401,7 +403,8 @@ fn create_new_window(
event_handlers: &WindowEventHandlers,
shortcut_manager: ShortcutRegistry,
) -> WebviewHandler {
let (webview, web_context, edit_queue) = webview::build(&mut cfg, event_loop, proxy.clone());
let (webview, web_context, asset_handlers, edit_queue) =
webview::build(&mut cfg, event_loop, proxy.clone());
let desktop_context = Rc::from(DesktopService::new(
webview,
proxy.clone(),
@@ -409,6 +412,7 @@ fn create_new_window(
queue.clone(),
event_handlers.clone(),
shortcut_manager,
asset_handlers,
edit_queue,
));
+195 -9
View File
@@ -1,18 +1,58 @@
use dioxus_core::ScopeState;
use dioxus_interpreter_js::{COMMON_JS, INTERPRETER_JS};
use slab::Slab;
use std::{
borrow::Cow,
future::Future,
ops::Deref,
path::{Path, PathBuf},
pin::Pin,
rc::Rc,
sync::Arc,
};
use tokio::{
runtime::Handle,
sync::{OnceCell, RwLock},
};
use wry::{
http::{status::StatusCode, Request, Response},
webview::RequestAsyncResponder,
Result,
};
use crate::{use_window, DesktopContext};
use crate::desktop_context::EditQueue;
static MINIFIED: &str = include_str!("./minified.js");
fn module_loader(root_name: &str, headless: bool) -> String {
let js = INTERPRETER_JS.replace(
"/*POST_HANDLE_EDITS*/",
r#"// Prevent file inputs from opening the file dialog on click
let inputs = document.querySelectorAll("input");
for (let input of inputs) {
if (!input.getAttribute("data-dioxus-file-listener")) {
// prevent file inputs from opening the file dialog on click
const type = input.getAttribute("type");
if (type === "file") {
input.setAttribute("data-dioxus-file-listener", true);
input.addEventListener("click", (event) => {
let target = event.target;
let target_id = find_real_id(target);
if (target_id !== null) {
const send = (event_name) => {
const message = serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name });
window.ipc.postMessage(message);
};
send("change&input");
}
event.preventDefault();
});
}
}
}"#,
);
format!(
r#"
<script type="module">
@@ -32,15 +72,158 @@ fn module_loader(root_name: &str, headless: bool) -> String {
)
}
pub(super) fn desktop_handler(
request: &Request<Vec<u8>>,
responder: RequestAsyncResponder,
/// An arbitrary asset is an HTTP response containing a binary body.
pub type AssetResponse = Response<Cow<'static, [u8]>>;
/// A future that returns an [`AssetResponse`]. This future may be spawned in a new thread,
/// so it must be [`Send`], [`Sync`], and `'static`.
pub trait AssetFuture: Future<Output = Option<AssetResponse>> + Send + Sync + 'static {}
impl<T: Future<Output = Option<AssetResponse>> + Send + Sync + 'static> AssetFuture for T {}
#[derive(Debug, Clone)]
/// A request for an asset. This is a wrapper around [`Request<Vec<u8>>`] that provides methods specific to asset requests.
pub struct AssetRequest {
path: PathBuf,
request: Arc<Request<Vec<u8>>>,
}
impl AssetRequest {
/// Get the path the asset request is for
pub fn path(&self) -> &Path {
&self.path
}
}
impl From<Request<Vec<u8>>> for AssetRequest {
fn from(request: Request<Vec<u8>>) -> Self {
let decoded = urlencoding::decode(request.uri().path().trim_start_matches('/'))
.expect("expected URL to be UTF-8 encoded");
let path = PathBuf::from(&*decoded);
Self {
request: Arc::new(request),
path,
}
}
}
impl Deref for AssetRequest {
type Target = Request<Vec<u8>>;
fn deref(&self) -> &Self::Target {
&self.request
}
}
/// A handler that takes an [`AssetRequest`] and returns a future that either loads the asset, or returns `None`.
/// This handler is stashed indefinitely in a context object, so it must be `'static`.
pub trait AssetHandler<F: AssetFuture>: Send + Sync + 'static {
/// Handle an asset request, returning a future that either loads the asset, or returns `None`
fn handle_request(&self, request: &AssetRequest) -> F;
}
impl<F: AssetFuture, T: Fn(&AssetRequest) -> F + Send + Sync + 'static> AssetHandler<F> for T {
fn handle_request(&self, request: &AssetRequest) -> F {
self(request)
}
}
type AssetHandlerRegistryInner =
Slab<Box<dyn Fn(&AssetRequest) -> Pin<Box<dyn AssetFuture>> + Send + Sync + 'static>>;
#[derive(Clone)]
pub struct AssetHandlerRegistry(Arc<RwLock<AssetHandlerRegistryInner>>);
impl AssetHandlerRegistry {
pub fn new() -> Self {
AssetHandlerRegistry(Arc::new(RwLock::new(Slab::new())))
}
pub async fn register_handler<F: AssetFuture>(&self, f: impl AssetHandler<F>) -> usize {
let mut registry = self.0.write().await;
registry.insert(Box::new(move |req| Box::pin(f.handle_request(req))))
}
pub async fn remove_handler(&self, id: usize) -> Option<()> {
let mut registry = self.0.write().await;
registry.try_remove(id).map(|_| ())
}
pub async fn try_handlers(&self, req: &AssetRequest) -> Option<AssetResponse> {
let registry = self.0.read().await;
for (_, handler) in registry.iter() {
if let Some(response) = handler(req).await {
return Some(response);
}
}
None
}
}
/// A handle to a registered asset handler.
pub struct AssetHandlerHandle {
desktop: DesktopContext,
handler_id: Rc<OnceCell<usize>>,
}
impl AssetHandlerHandle {
/// Returns the ID for this handle.
///
/// Because registering an ID is asynchronous, this may return `None` if the
/// registration has not completed yet.
pub fn handler_id(&self) -> Option<usize> {
self.handler_id.get().copied()
}
}
impl Drop for AssetHandlerHandle {
fn drop(&mut self) {
let cell = Rc::clone(&self.handler_id);
let desktop = Rc::clone(&self.desktop);
tokio::task::block_in_place(move || {
Handle::current().block_on(async move {
if let Some(id) = cell.get() {
desktop.asset_handlers.remove_handler(*id).await;
}
})
});
}
}
/// Provide a callback to handle asset loading yourself.
///
/// The callback takes a path as requested by the web view, and it should return `Some(response)`
/// if you want to load the asset, and `None` if you want to fallback on the default behavior.
pub fn use_asset_handler<F: AssetFuture>(
cx: &ScopeState,
handler: impl AssetHandler<F>,
) -> &AssetHandlerHandle {
let desktop = Rc::clone(use_window(cx));
cx.use_hook(|| {
let handler_id = Rc::new(OnceCell::new());
let handler_id_ref = Rc::clone(&handler_id);
let desktop_ref = Rc::clone(&desktop);
cx.push_future(async move {
let id = desktop.asset_handlers.register_handler(handler).await;
handler_id.set(id).unwrap();
});
AssetHandlerHandle {
desktop: desktop_ref,
handler_id: handler_id_ref,
}
})
}
pub(super) async fn desktop_handler(
request: Request<Vec<u8>>,
custom_head: Option<String>,
custom_index: Option<String>,
root_name: &str,
asset_handlers: &AssetHandlerRegistry,
edit_queue: &EditQueue,
headless: bool,
) {
) -> Result<AssetResponse> {
let request = AssetRequest::from(request);
// If the request is for the root, we'll serve the index.html file.
if request.uri().path() == "/" {
// If a custom index is provided, just defer to that, expecting the user to know what they're doing.
@@ -86,18 +269,21 @@ pub(super) fn desktop_handler(
return;
}
// If the user provided a custom asset handler, then call it and return the response
// if the request was handled.
if let Some(response) = asset_handlers.try_handlers(&request).await {
return Ok(response);
}
// Else, try to serve a file from the filesystem.
let decoded = urlencoding::decode(request.uri().path().trim_start_matches('/'))
.expect("expected URL to be UTF-8 encoded");
let path = PathBuf::from(&*decoded);
// If the path is relative, we'll try to serve it from the assets directory.
let mut asset = get_asset_root()
.unwrap_or_else(|| Path::new(".").to_path_buf())
.join(&path);
.join(&request.path);
if !asset.exists() {
asset = PathBuf::from("/").join(path);
asset = PathBuf::from("/").join(request.path);
}
if asset.exists() {
+93 -13
View File
@@ -1,17 +1,19 @@
use crate::desktop_context::{EditQueue, EventData};
use crate::protocol;
use crate::protocol::{self, AssetHandlerRegistry};
use crate::{desktop_context::UserWindowEvent, Config};
use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
pub use wry;
pub use wry::application as tao;
use wry::application::menu::{MenuBar, MenuItem};
use wry::application::window::Window;
use wry::http::Response;
use wry::webview::{WebContext, WebView, WebViewBuilder};
pub(crate) fn build(
cfg: &mut Config,
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
proxy: EventLoopProxy<UserWindowEvent>,
) -> (WebView, WebContext, EditQueue) {
) -> (WebView, WebContext, AssetHandlerRegistry, EditQueue) {
let builder = cfg.window.clone();
let window = builder.with_visible(false).build(event_loop).unwrap();
let file_handler = cfg.file_drop_handler.take();
@@ -19,6 +21,12 @@ pub(crate) fn build(
let index_file = cfg.custom_index.clone();
let root_name = cfg.root_name.clone();
if cfg.enable_default_menu_bar {
builder = builder.with_menu(build_default_menu_bar());
}
let window = builder.with_visible(false).build(event_loop).unwrap();
// We assume that if the icon is None in cfg, then the user just didnt set it
if cfg.window.window.window_icon.is_none() {
window.set_window_icon(Some(
@@ -34,6 +42,8 @@ pub(crate) fn build(
let mut web_context = WebContext::new(cfg.data_dir.clone());
let edit_queue = EditQueue::default();
let headless = !cfg.window.window.visible;
let asset_handlers = AssetHandlerRegistry::new();
let asset_handlers_ref = asset_handlers.clone();
let mut webview = WebViewBuilder::new(window)
.unwrap()
@@ -46,19 +56,22 @@ pub(crate) fn build(
_ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window.id()));
}
})
.with_asynchronous_custom_protocol("dioxus".into(), {
let edit_queue = edit_queue.clone();
move |r, responder| {
protocol::desktop_handler(
&r,
responder,
.with_asynchronous_custom_protocol(String::from("dioxus"), move |request, responder| {
let custom_head = custom_head.clone();
let index_file = index_file.clone();
let root_name = root_name.clone();
let asset_handlers_ref = asset_handlers_ref.clone();
tokio::spawn(async move {
let response_res = protocol::desktop_handler(
request,
custom_head.clone(),
index_file.clone(),
&root_name,
&edit_queue,
headless,
&asset_handlers_ref,
)
}
.await;
responder.respond(response);
});
})
.with_file_drop_handler(move |window, evet| {
file_handler
@@ -84,7 +97,16 @@ pub(crate) fn build(
// .with_web_context(&mut web_context);
for (name, handler) in cfg.protocols.drain(..) {
webview = webview.with_custom_protocol(name, handler)
webview = webview.with_custom_protocol(name, move |r| match handler(&r) {
Ok(response) => response,
Err(err) => {
tracing::error!("Error: {}", err);
Response::builder()
.status(500)
.body(err.to_string().into_bytes().into())
.unwrap()
}
})
}
if cfg.disable_context_menu {
@@ -107,5 +129,63 @@ pub(crate) fn build(
webview = webview.with_devtools(true);
}
(webview.build().unwrap(), web_context, edit_queue)
(webview.build().unwrap(), web_context, asset_handlers, edit_queue)
}
/// Builds a standard menu bar depending on the users platform. It may be used as a starting point
/// to further customize the menu bar and pass it to a [`WindowBuilder`](tao::window::WindowBuilder).
/// > Note: The default menu bar enables macOS shortcuts like cut/copy/paste.
/// > The menu bar differs per platform because of constraints introduced
/// > by [`MenuItem`](tao::menu::MenuItem).
pub fn build_default_menu_bar() -> MenuBar {
let mut menu_bar = MenuBar::new();
// since it is uncommon on windows to have an "application menu"
// we add a "window" menu to be more consistent across platforms with the standard menu
let mut window_menu = MenuBar::new();
#[cfg(target_os = "macos")]
{
window_menu.add_native_item(MenuItem::EnterFullScreen);
window_menu.add_native_item(MenuItem::Zoom);
window_menu.add_native_item(MenuItem::Separator);
}
window_menu.add_native_item(MenuItem::Hide);
#[cfg(target_os = "macos")]
{
window_menu.add_native_item(MenuItem::HideOthers);
window_menu.add_native_item(MenuItem::ShowAll);
}
window_menu.add_native_item(MenuItem::Minimize);
window_menu.add_native_item(MenuItem::CloseWindow);
window_menu.add_native_item(MenuItem::Separator);
window_menu.add_native_item(MenuItem::Quit);
menu_bar.add_submenu("Window", true, window_menu);
// since tao supports none of the below items on linux we should only add them on macos/windows
#[cfg(not(target_os = "linux"))]
{
let mut edit_menu = MenuBar::new();
#[cfg(target_os = "macos")]
{
edit_menu.add_native_item(MenuItem::Undo);
edit_menu.add_native_item(MenuItem::Redo);
edit_menu.add_native_item(MenuItem::Separator);
}
edit_menu.add_native_item(MenuItem::Cut);
edit_menu.add_native_item(MenuItem::Copy);
edit_menu.add_native_item(MenuItem::Paste);
#[cfg(target_os = "macos")]
{
edit_menu.add_native_item(MenuItem::Separator);
edit_menu.add_native_item(MenuItem::SelectAll);
}
menu_bar.add_submenu("Edit", true, edit_menu);
}
menu_bar
}
@@ -37,7 +37,7 @@ fn app(cx: Scope) -> Element {
// todo: remove
let mut trimmed = format!("{event:?}");
trimmed.truncate(200);
rsx!(p { "{trimmed}" })
rsx!( p { "{trimmed}" } )
});
let log_event = move |event: Event| {
@@ -45,10 +45,7 @@ fn app(cx: Scope) -> Element {
};
cx.render(rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
div { width: "100%", height: "100%", flex_direction: "column",
div {
width: "80%",
height: "50%",
@@ -59,7 +56,7 @@ fn app(cx: Scope) -> Element {
onmousemove: move |event| log_event(Event::MouseMove(event.inner().clone())),
onclick: move |event| log_event(Event::MouseClick(event.inner().clone())),
ondblclick: move |event| log_event(Event::MouseDoubleClick(event.inner().clone())),
ondoubleclick: move |event| log_event(Event::MouseDoubleClick(event.inner().clone())),
onmousedown: move |event| log_event(Event::MouseDown(event.inner().clone())),
onmouseup: move |event| log_event(Event::MouseUp(event.inner().clone())),
@@ -73,13 +70,8 @@ fn app(cx: Scope) -> Element {
onfocusout: move |event| log_event(Event::FocusOut(event.inner().clone())),
"Hover, click, type or scroll to see the info down below"
},
div {
width: "80%",
height: "50%",
flex_direction: "column",
events_rendered,
},
},
}
div { width: "80%", height: "50%", flex_direction: "column", events_rendered }
}
})
}
+1 -1
View File
@@ -26,7 +26,7 @@ tower = { version = "0.4.13", features = ["util"], optional = true }
axum-macros = "0.3.7"
# salvo
salvo = { version = "0.46.0", optional = true, features = ["serve-static", "websocket", "compression"] }
salvo = { version = "0.63.0", optional = true, features = ["serve-static", "websocket", "compression"] }
serde = "1.0.159"
# Dioxus + SSR
@@ -12,7 +12,7 @@ dioxus = { workspace = true }
dioxus-fullstack = { workspace = true }
tokio = { workspace = true, features = ["full"], optional = true }
serde = "1.0.159"
salvo = { version = "0.37.9", optional = true }
salvo = { version = "0.63.0", optional = true }
execute = "0.2.12"
reqwest = "0.11.18"
simple_logger = "4.2.0"
@@ -12,9 +12,10 @@ use serde::{de::DeserializeOwned, Serialize};
/// use dioxus_fullstack::prelude::*;
///
/// fn app(cx: Scope) -> Element {
/// let state1 = use_state(cx, || from_server(|| {
/// let state1 = use_state(cx, || server_cached(|| {
/// 1234
/// }));
/// todo!()
/// }
/// ```
pub fn server_cached<O: 'static + Serialize + DeserializeOwned>(server_fn: impl Fn() -> O) -> O {
+4 -2
View File
@@ -700,14 +700,16 @@ impl Owner {
/// Creates an invalid handle. This is useful for creating a handle that will be filled in later. If you use this before the value is filled in, you will get may get a panic or an out of date value.
pub fn invalid<T: 'static>(&self) -> GenerationalBox<T> {
let location = self.store.claim();
GenerationalBox {
let key = GenerationalBox {
raw: location,
#[cfg(any(debug_assertions, feature = "check_generation"))]
generation: location.0.generation.get(),
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
created_at: std::panic::Location::caller(),
_marker: PhantomData,
}
};
self.owned.borrow_mut().push(location);
key
}
}
+8 -10
View File
@@ -99,9 +99,8 @@ where
// Create the new future
let return_value = future(dependencies.out());
if let Some(task) = return_value.apply(state.cleanup.clone(), cx) {
state.task.set(Some(task));
}
let task = return_value.apply(state.cleanup.clone(), cx);
state.task.set(Some(task));
}
}
@@ -109,15 +108,15 @@ 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>;
fn apply(self, oncleanup: UseEffectCleanup, cx: &ScopeState) -> TaskId;
}
impl<T> UseEffectReturn<()> for T
where
T: Future<Output = ()> + 'static,
{
fn apply(self, _: UseEffectCleanup, cx: &ScopeState) -> Option<TaskId> {
Some(cx.push_future(self))
fn apply(self, _: UseEffectCleanup, cx: &ScopeState) -> TaskId {
cx.push_future(self)
}
}
@@ -128,12 +127,11 @@ 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 {
fn apply(self, oncleanup: UseEffectCleanup, cx: &ScopeState) -> TaskId {
cx.push_future(async move {
let cleanup = self.await;
*oncleanup.borrow_mut() = Some(Box::new(cleanup) as Box<dyn FnOnce()>);
});
Some(task)
})
}
}
+2 -1
View File
@@ -21,7 +21,7 @@ keyboard-types = "0.7"
async-trait = "0.1.58"
serde-value = "0.7.0"
tokio = { workspace = true, features = ["fs", "io-util"], optional = true }
rfd = { version = "0.11.3", optional = true }
rfd = { version = "0.12", optional = true }
async-channel = "1.8.0"
serde_json = { version = "1", optional = true }
@@ -68,3 +68,4 @@ mounted = [
wasm-bind = ["web-sys", "wasm-bindgen"]
native-bind = ["tokio"]
hot-reload-context = ["dioxus-rsx"]
html-to-rsx = []
+53 -3
View File
@@ -79,6 +79,25 @@ macro_rules! impl_attribute_match {
};
}
#[cfg(feature = "html-to-rsx")]
macro_rules! impl_html_to_rsx_attribute_match {
(
$attr:ident $fil:ident $name:literal
) => {
if $attr == $name {
return Some(stringify!($fil));
}
};
(
$attr:ident $fil:ident $_:tt
) => {
if $attr == stringify!($fil) {
return Some(stringify!($fil));
}
};
}
macro_rules! impl_element {
(
$(#[$attr:meta])*
@@ -316,6 +335,38 @@ macro_rules! builder_constructors {
}
}
#[cfg(feature = "html-to-rsx")]
pub fn map_html_attribute_to_rsx(html: &str) -> Option<&'static str> {
$(
$(
impl_html_to_rsx_attribute_match!(
html $fil $extra
);
)*
)*
if let Some(name) = crate::map_html_global_attributes_to_rsx(html) {
return Some(name);
}
if let Some(name) = crate::map_html_svg_attributes_to_rsx(html) {
return Some(name);
}
None
}
#[cfg(feature = "html-to-rsx")]
pub fn map_html_element_to_rsx(html: &str) -> Option<&'static str> {
$(
if html == stringify!($name) {
return Some(stringify!($name));
}
)*
None
}
$(
impl_element!(
$(#[$attr])*
@@ -998,9 +1049,8 @@ builder_constructors! {
src: Uri DEFAULT,
text: String DEFAULT,
// r#async: Bool,
// r#type: String, // TODO could be an enum
r#type: String "type",
r#async: Bool "async",
r#type: String "type", // TODO could be an enum
r#script: String "script",
};
+17 -4
View File
@@ -119,10 +119,7 @@ impl_event! {
/// oncontextmenu
oncontextmenu
/// ondoubleclick
ondoubleclick
/// ondoubleclick
#[deprecated(since = "0.5.0", note = "use ondoubleclick instead")]
ondblclick
/// onmousedown
@@ -149,6 +146,22 @@ impl_event! {
onmouseup
}
/// ondoubleclick
#[inline]
pub fn ondoubleclick<'a, E: crate::EventReturn<T>, T>(
_cx: &'a ::dioxus_core::ScopeState,
mut _f: impl FnMut(::dioxus_core::Event<MouseData>) -> E + 'a,
) -> ::dioxus_core::Attribute<'a> {
::dioxus_core::Attribute::new(
"ondblclick",
_cx.listener(move |e: ::dioxus_core::Event<MouseData>| {
_f(e).spawn(_cx);
}),
None,
false,
)
}
impl MouseData {
/// Construct MouseData with the specified properties
///
+46
View File
@@ -33,12 +33,44 @@ macro_rules! trait_method_mapping {
};
}
#[cfg(feature = "html-to-rsx")]
macro_rules! html_to_rsx_attribute_mapping {
(
$matching:ident;
$(#[$attr:meta])*
$name:ident;
) => {
if $matching == stringify!($name) {
return Some(stringify!($name));
}
};
(
$matching:ident;
$(#[$attr:meta])*
$name:ident: $lit:literal;
) => {
if $matching == stringify!($lit) {
return Some(stringify!($name));
}
};
(
$matching:ident;
$(#[$attr:meta])*
$name:ident: $lit:literal, $ns:literal;
) => {
if $matching == stringify!($lit) {
return Some(stringify!($name));
}
};
}
macro_rules! trait_methods {
(
@base
$(#[$trait_attr:meta])*
$trait:ident;
$fn:ident;
$fn_html_to_rsx:ident;
$(
$(#[$attr:meta])*
$name:ident $(: $($arg:literal),*)*;
@@ -62,6 +94,18 @@ macro_rules! trait_methods {
)*
None
}
#[cfg(feature = "html-to-rsx")]
#[doc = "Converts an HTML attribute to an RSX attribute"]
pub(crate) fn $fn_html_to_rsx(html: &str) -> Option<&'static str> {
$(
html_to_rsx_attribute_mapping! {
html;
$name$(: $($arg),*)*;
}
)*
None
}
};
// Rename the incoming ident and apply a custom namespace
@@ -79,6 +123,7 @@ trait_methods! {
GlobalAttributes;
map_global_attributes;
map_html_global_attributes_to_rsx;
/// Prevent the default action for this element.
///
@@ -1593,6 +1638,7 @@ trait_methods! {
@base
SvgAttributes;
map_svg_attributes;
map_html_svg_attributes_to_rsx;
/// Prevent the default action for this element.
///
+2
View File
@@ -19,6 +19,8 @@
mod elements;
#[cfg(feature = "hot-reload-context")]
pub use elements::HtmlCtx;
#[cfg(feature = "html-to-rsx")]
pub use elements::{map_html_attribute_to_rsx, map_html_element_to_rsx};
pub mod events;
pub mod geometry;
mod global_attributes;
+13 -2
View File
@@ -35,10 +35,14 @@ warp = { version = "0.3.3", optional = true }
axum = { version = "0.6.1", optional = true, features = ["ws"] }
# salvo
salvo = { version = "0.44.1", optional = true, features = ["ws"] }
salvo = { version = "0.63.0", optional = true, features = ["websocket"] }
once_cell = "1.17.1"
async-trait = "0.1.71"
# rocket
rocket = { version = "0.5.0", optional = true }
rocket_ws = { version = "0.1.0", optional = true }
# actix is ... complicated?
# actix-files = { version = "0.6.2", optional = true }
# actix-web = { version = "4.2.1", optional = true }
@@ -50,7 +54,9 @@ tokio = { workspace = true, features = ["full"] }
dioxus = { workspace = true }
warp = "0.3.3"
axum = { version = "0.6.1", features = ["ws"] }
salvo = { version = "0.44.1", features = ["affix", "ws"] }
salvo = { version = "0.63.0", features = ["affix", "websocket"] }
rocket = "0.5.0"
rocket_ws = "0.1.0"
tower = "0.4.13"
[build-dependencies]
@@ -61,6 +67,7 @@ minify-js = "0.5.6"
default = ["hot-reload"]
# actix = ["actix-files", "actix-web", "actix-ws"]
hot-reload = ["dioxus-hot-reload"]
rocket = ["dep:rocket", "dep:rocket_ws"]
[[example]]
name = "axum"
@@ -77,3 +84,7 @@ required-features = ["salvo"]
[[example]]
name = "warp"
required-features = ["warp"]
[[example]]
name = "rocket"
required-features = ["rocket"]
+1
View File
@@ -28,6 +28,7 @@ The current backend frameworks supported include:
- Axum
- Warp
- Salvo
- Rocket
Dioxus-LiveView exports some primitives to wire up an app into an existing backend framework.
+76
View File
@@ -0,0 +1,76 @@
#[macro_use]
extern crate rocket;
use dioxus::prelude::*;
use dioxus_liveview::LiveViewPool;
use rocket::response::content::RawHtml;
use rocket::{Config, Rocket, State};
use rocket_ws::{Channel, WebSocket};
fn app(cx: Scope) -> Element {
let mut num = use_state(cx, || 0);
cx.render(rsx! {
div {
"hello Rocket! {num}"
button { onclick: move |_| num += 1, "Increment" }
}
})
}
fn index_page_with_glue(glue: &str) -> RawHtml<String> {
RawHtml(format!(
r#"
<!DOCTYPE html>
<html>
<head> <title>Dioxus LiveView with Rocket</title> </head>
<body> <div id="main"></div> </body>
{glue}
</html>
"#,
glue = glue
))
}
#[get("/")]
async fn index(config: &Config) -> RawHtml<String> {
index_page_with_glue(&dioxus_liveview::interpreter_glue(&format!(
"ws://{addr}:{port}/ws",
addr = config.address,
port = config.port,
)))
}
#[get("/as-path")]
async fn as_path() -> RawHtml<String> {
index_page_with_glue(&dioxus_liveview::interpreter_glue("/ws"))
}
#[get("/ws")]
fn ws(ws: WebSocket, pool: &State<LiveViewPool>) -> Channel<'static> {
let pool = pool.inner().to_owned();
ws.channel(move |stream| {
Box::pin(async move {
let _ = pool
.launch(dioxus_liveview::rocket_socket(stream), app)
.await;
Ok(())
})
})
}
#[tokio::main]
async fn main() {
let view = dioxus_liveview::LiveViewPool::new();
Rocket::build()
.manage(view)
.mount("/", routes![index, as_path, ws])
.ignite()
.await
.expect("Failed to ignite rocket")
.launch()
.await
.expect("Failed to launch rocket");
}
@@ -0,0 +1,25 @@
use crate::{LiveViewError, LiveViewSocket};
use rocket::futures::{SinkExt, StreamExt};
use rocket_ws::{result::Error, stream::DuplexStream, Message};
/// Convert a rocket websocket into a LiveViewSocket
///
/// This is required to launch a LiveView app using the rocket web framework
pub fn rocket_socket(stream: DuplexStream) -> impl LiveViewSocket {
stream
.map(transform_rx)
.with(transform_tx)
.sink_map_err(|_| LiveViewError::SendingFailed)
}
fn transform_rx(message: Result<Message, Error>) -> Result<Vec<u8>, LiveViewError> {
message
.map_err(|_| LiveViewError::SendingFailed)?
.into_text()
.map(|s| s.into_bytes())
.map_err(|_| LiveViewError::SendingFailed)
}
async fn transform_tx(message: Vec<u8>) -> Result<Message, Error> {
Ok(Message::Text(String::from_utf8_lossy(&message).to_string()))
}
+5
View File
@@ -18,6 +18,11 @@ pub mod adapters {
#[cfg(feature = "salvo")]
pub use salvo_adapter::*;
#[cfg(feature = "rocket")]
pub mod rocket_adapter;
#[cfg(feature = "rocket")]
pub use rocket_adapter::*;
}
pub use adapters::*;
+24 -26
View File
@@ -3,7 +3,6 @@ use dioxus_core::*;
use dioxus_native_core::prelude::*;
use dioxus_native_core_macro::partial_derive_state;
use shipyard::Component;
use std::cell::Cell;
fn random_ns() -> Option<&'static str> {
let namespace = rand::random::<u8>() % 2;
@@ -178,22 +177,23 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode {
let range = if depth > 3 { 1 } else { 3 };
match rand::random::<u8>() % range {
0 => DynamicNode::Placeholder(Default::default()),
1 => cx.make_node((0..(rand::random::<u8>() % 5)).map(|_| VNode {
key: None,
parent: Default::default(),
template: Cell::new(Template {
name: concat!(file!(), ":", line!(), ":", column!(), ":0"),
roots: &[TemplateNode::Dynamic { id: 0 }],
node_paths: &[&[0]],
attr_paths: &[],
}),
root_ids: dioxus::core::exports::bumpalo::collections::Vec::new_in(cx.bump()).into(),
dynamic_nodes: cx.bump().alloc([cx.component(
create_random_element,
DepthProps { depth, root: false },
"create_random_element",
)]),
dynamic_attrs: &[],
1 => cx.make_node((0..(rand::random::<u8>() % 5)).map(|_| {
VNode::new(
None,
Template {
name: concat!(file!(), ":", line!(), ":", column!(), ":0"),
roots: &[TemplateNode::Dynamic { id: 0 }],
node_paths: &[&[0]],
attr_paths: &[],
},
dioxus::core::exports::bumpalo::collections::Vec::new_in(cx.bump()),
cx.bump().alloc([cx.component(
create_random_element,
DepthProps { depth, root: false },
"create_random_element",
)]),
&[],
)
})),
2 => cx.component(
create_random_element,
@@ -253,13 +253,11 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
.into_boxed_str(),
));
println!("{template:#?}");
let node = VNode {
key: None,
parent: None,
template: Cell::new(template),
root_ids: dioxus::core::exports::bumpalo::collections::Vec::new_in(cx.bump())
.into(),
dynamic_nodes: {
let node = VNode::new(
None,
template,
dioxus::core::exports::bumpalo::collections::Vec::new_in(cx.bump()),
{
let dynamic_nodes: Vec<_> = dynamic_node_types
.iter()
.map(|ty| match ty {
@@ -273,12 +271,12 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
.collect();
cx.bump().alloc(dynamic_nodes)
},
dynamic_attrs: cx.bump().alloc(
cx.bump().alloc(
(0..template.attr_paths.len())
.map(|_| create_random_dynamic_attr(cx))
.collect::<Vec<_>>(),
),
};
);
Some(node)
}
_ => None,
+2 -2
View File
@@ -92,10 +92,10 @@ impl State for TaffyLayout {
attribute, value, ..
} in attributes
{
if let Some(text) = value.as_text() {
if value.as_custom().is_none() {
apply_layout_attributes_cfg(
&attribute.name,
text,
&value.to_string(),
&mut style,
&LayoutConfigeration {
border_widths: BorderWidths {
+1
View File
@@ -15,6 +15,7 @@ keywords = ["dom", "ui", "gui", "react"]
[dependencies]
dioxus-autofmt = { workspace = true }
dioxus-rsx = { workspace = true }
dioxus-html = { workspace = true, features = ["html-to-rsx"]}
html_parser.workspace = true
proc-macro2 = "1.0.49"
quote = "1.0.23"
+26 -10
View File
@@ -3,6 +3,7 @@
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
use convert_case::{Case, Casing};
use dioxus_html::{map_html_attribute_to_rsx, map_html_element_to_rsx};
use dioxus_rsx::{
BodyNode, CallBody, Component, Element, ElementAttr, ElementAttrNamed, ElementName, IfmtInput,
};
@@ -24,26 +25,41 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
match node {
Node::Text(text) => Some(BodyNode::Text(ifmt_from_text(text))),
Node::Element(el) => {
let el_name = el.name.to_case(Case::Snake);
let el_name = ElementName::Ident(Ident::new(el_name.as_str(), Span::call_site()));
let el_name = if let Some(name) = map_html_element_to_rsx(&el.name) {
ElementName::Ident(Ident::new(name, Span::call_site()))
} else {
// if we don't recognize it and it has a dash, we assume it's a web component
if el.name.contains('-') {
ElementName::Custom(LitStr::new(&el.name, Span::call_site()))
} else {
// otherwise, it might be an element that isn't supported yet
ElementName::Ident(Ident::new(&el.name.to_case(Case::Snake), Span::call_site()))
}
};
let mut attributes: Vec<_> = el
.attributes
.iter()
.map(|(name, value)| {
let ident = if matches!(name.as_str(), "for" | "async" | "type" | "as") {
Ident::new_raw(name.as_str(), Span::call_site())
let value = ifmt_from_text(value.as_deref().unwrap_or("false"));
let attr = if let Some(name) = map_html_attribute_to_rsx(name) {
let ident = if let Some(name) = name.strip_prefix("r#") {
Ident::new_raw(name, Span::call_site())
} else {
Ident::new(name, Span::call_site())
};
ElementAttr::AttrText { value, name: ident }
} else {
let new_name = name.to_case(Case::Snake);
Ident::new(new_name.as_str(), Span::call_site())
// If we don't recognize the attribute, we assume it's a custom attribute
ElementAttr::CustomAttrText {
value,
name: LitStr::new(name, Span::call_site()),
}
};
ElementAttrNamed {
el_name: el_name.clone(),
attr: ElementAttr::AttrText {
value: ifmt_from_text(value.as_deref().unwrap_or("false")),
name: ident,
},
attr,
}
})
.collect();
+33
View File
@@ -0,0 +1,33 @@
use html_parser::Dom;
#[test]
fn h_tags_translate() {
let html = r#"
<div>
<h1>hello world!</h1>
<h2>hello world!</h2>
<h3>hello world!</h3>
<h4>hello world!</h4>
<h5>hello world!</h5>
<h6>hello world!</h6>
</div>
"#
.trim();
let dom = Dom::parse(html).unwrap();
let body = rsx_rosetta::rsx_from_html(&dom);
let out = dioxus_autofmt::write_block_out(body).unwrap();
let expected = r#"
div {
h1 { "hello world!" }
h2 { "hello world!" }
h3 { "hello world!" }
h4 { "hello world!" }
h5 { "hello world!" }
h6 { "hello world!" }
}"#;
pretty_assertions::assert_eq!(&out, &expected);
}
+21
View File
@@ -0,0 +1,21 @@
use html_parser::Dom;
#[test]
fn raw_attribute() {
let html = r#"
<div>
<div unrecognizedattribute="asd">hello world!</div>
</div>
"#
.trim();
let dom = Dom::parse(html).unwrap();
let body = rsx_rosetta::rsx_from_html(&dom);
let out = dioxus_autofmt::write_block_out(body).unwrap();
let expected = r#"
div { div { "unrecognizedattribute": "asd", "hello world!" } }"#;
pretty_assertions::assert_eq!(&out, &expected);
}
-4
View File
@@ -9,8 +9,6 @@ fn simple_elements() {
<div id="asd">hello world!</div>
<div for="asd">hello world!</div>
<div async="asd">hello world!</div>
<div LargeThing="asd">hello world!</div>
<ai-is-awesome>hello world!</ai-is-awesome>
</div>
"#
.trim();
@@ -28,8 +26,6 @@ fn simple_elements() {
div { id: "asd", "hello world!" }
div { r#for: "asd", "hello world!" }
div { r#async: "asd", "hello world!" }
div { large_thing: "asd", "hello world!" }
ai_is_awesome { "hello world!" }
}"#;
pretty_assertions::assert_eq!(&out, &expected);
}
@@ -0,0 +1,21 @@
use html_parser::Dom;
#[test]
fn web_components_translate() {
let html = r#"
<div>
<my-component></my-component>
</div>
"#
.trim();
let dom = Dom::parse(html).unwrap();
let body = rsx_rosetta::rsx_from_html(&dom);
let out = dioxus_autofmt::write_block_out(body).unwrap();
let expected = r#"
div { my-component {} }"#;
pretty_assertions::assert_eq!(&out, &expected);
}
+18 -14
View File
@@ -204,12 +204,17 @@ impl<'a> ToTokens for TemplateRenderer<'a> {
None => quote! { None },
};
let spndbg = format!("{:?}", self.roots[0].span());
let root_col = spndbg
.rsplit_once("..")
.and_then(|(_, after)| after.split_once(')').map(|(before, _)| before))
.unwrap_or_default();
let root_col = match self.roots.first() {
Some(first_root) => {
let first_root_span = format!("{:?}", first_root.span());
first_root_span
.rsplit_once("..")
.and_then(|(_, after)| after.split_once(')').map(|(before, _)| before))
.unwrap_or_default()
.to_string()
}
_ => "0".to_string(),
};
let root_printer = self.roots.iter().enumerate().map(|(idx, root)| {
context.current_path.push(idx as u8);
let out = context.render_static_node(root);
@@ -247,14 +252,13 @@ impl<'a> ToTokens for TemplateRenderer<'a> {
node_paths: &[ #(#node_paths),* ],
attr_paths: &[ #(#attr_paths),* ],
};
::dioxus::core::VNode {
parent: None,
key: #key_tokens,
template: std::cell::Cell::new(TEMPLATE),
root_ids: dioxus::core::exports::bumpalo::collections::Vec::with_capacity_in(#root_count, __cx.bump()).into(),
dynamic_nodes: __cx.bump().alloc([ #( #node_printer ),* ]),
dynamic_attrs: __cx.bump().alloc([ #( #dyn_attr_printer ),* ]),
}
::dioxus::core::VNode::new(
#key_tokens,
TEMPLATE,
dioxus::core::exports::bumpalo::collections::Vec::with_capacity_in(#root_count, __cx.bump()),
__cx.bump().alloc([ #( #node_printer ),* ]),
__cx.bump().alloc([ #( #dyn_attr_printer ),* ]),
)
});
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ use syn::{
/// are enabled), it will instead make a network request to the server.
///
/// You can specify one, two, or three arguments to the server function:
/// 1. **Required**: A type name that will be used to identify and register the server function
/// 1. *Optional*: A type name that will be used to identify and register the server function
/// (e.g., `MyServerFn`).
/// 2. *Optional*: A URL prefix at which the function will be mounted when it’s registered
/// (e.g., `"/api"`). Defaults to `"/"`.
+6
View File
@@ -495,3 +495,9 @@ impl<T> Deref for ReadOnlySignal<T> {
reference_to_closure as &Self::Target
}
}
impl<T> From<Signal<T>> for ReadOnlySignal<T> {
fn from(signal: Signal<T>) -> Self {
Self::new(signal)
}
}
+15 -1
View File
@@ -67,6 +67,7 @@ pub struct IncrementalRendererConfig {
memory_cache_limit: usize,
invalidate_after: Option<Duration>,
map_path: Option<PathMapFn>,
clear_cache: bool,
}
impl Default for IncrementalRendererConfig {
@@ -83,9 +84,16 @@ impl IncrementalRendererConfig {
memory_cache_limit: 10000,
invalidate_after: None,
map_path: None,
clear_cache: true,
}
}
/// Clear the cache on startup (default: true)
pub fn clear_cache(mut self, clear_cache: bool) -> Self {
self.clear_cache = clear_cache;
self
}
/// Set a mapping from the route to the file path. This will override the default mapping configured with `static_dir`.
/// The function should return the path to the folder to store the index.html file in.
pub fn map_path<F: Fn(&str) -> PathBuf + Send + Sync + 'static>(mut self, map_path: F) -> Self {
@@ -114,7 +122,7 @@ impl IncrementalRendererConfig {
/// Build the incremental renderer.
pub fn build(self) -> IncrementalRenderer {
let static_dir = self.static_dir.clone();
IncrementalRenderer {
let mut renderer = IncrementalRenderer {
static_dir: self.static_dir.clone(),
memory_cache: NonZeroUsize::new(self.memory_cache_limit)
.map(|limit| lru::LruCache::with_hasher(limit, Default::default())),
@@ -129,6 +137,12 @@ impl IncrementalRendererConfig {
path
})
}),
};
if self.clear_cache {
renderer.invalidate_all();
}
renderer
}
}
+88
View File
@@ -238,6 +238,94 @@ fn to_string_works() {
assert_eq!(out, "<div class=\"asdasdasd\" class=\"asdasdasd\" id=\"id-123\">Hello world 1 --&gt;123&lt;-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div>&lt;/diiiiiiiiv&gt;<div>finalize 0</div><div>finalize 1</div><div>finalize 2</div><div>finalize 3</div><div>finalize 4</div></div>");
}
#[test]
fn empty_for_loop_works() {
use dioxus::prelude::*;
fn app(cx: Scope) -> Element {
render! {
div { class: "asdasdasd",
for _ in (0..5) {
}
}
}
}
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
let mut renderer = Renderer::new();
let out = renderer.render(&dom);
for item in renderer.template_cache.iter() {
if item.1.segments.len() > 5 {
assert_eq!(
item.1.segments,
vec![
PreRendered("<div class=\"asdasdasd\"".into(),),
Attr(0,),
StyleMarker {
inside_style_tag: false,
},
PreRendered(">".into()),
InnerHtmlMarker,
PreRendered("</div>".into(),),
]
);
}
}
use Segment::*;
assert_eq!(out, "<div class=\"asdasdasd\"></div>");
}
#[test]
fn empty_render_works() {
use dioxus::prelude::*;
fn app(cx: Scope) -> Element {
render! {}
}
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
let mut renderer = Renderer::new();
let out = renderer.render(&dom);
for item in renderer.template_cache.iter() {
if item.1.segments.len() > 5 {
assert_eq!(item.1.segments, vec![]);
}
}
assert_eq!(out, "");
}
#[test]
fn empty_rsx_works() {
use dioxus::prelude::*;
fn app(_: Scope) -> Element {
rsx! {};
None
}
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
let mut renderer = Renderer::new();
let out = renderer.render(&dom);
for item in renderer.template_cache.iter() {
if item.1.segments.len() > 5 {
assert_eq!(item.1.segments, vec![]);
}
}
assert_eq!(out, "");
}
pub(crate) const BOOL_ATTRS: &[&str] = &[
"allowfullscreen",
"allowpaymentrequest",
+1
View File
@@ -42,6 +42,7 @@ features = [
"HtmlFormElement",
"Text",
"Window",
"console",
]
[features]
+1 -1
View File
@@ -190,7 +190,7 @@ pub static BUILTIN_INTERNED_STRINGS: &[&str] = &[
"oncopy",
"oncuechange",
"oncut",
"ondblclick",
"ondoubleclick",
"ondrag",
"ondragend",
"ondragenter",
+25 -12
View File
@@ -45,7 +45,16 @@ impl WebsysDom {
let document = load_document();
let root = match document.get_element_by_id(&cfg.rootname) {
Some(root) => root,
None => document.create_element("body").ok().unwrap(),
None => {
web_sys::console::error_1(
&format!(
"element '#{}' not found. mounting to the body.",
cfg.rootname
)
.into(),
);
document.create_element("body").ok().unwrap()
}
};
let interpreter = Channel::default();
@@ -247,17 +256,21 @@ impl WebsysDom {
i.flush();
for id in to_mount {
let node = get_node(id.0 as u32);
if let Some(element) = node.dyn_ref::<Element>() {
let data: MountedData = element.into();
let data = Rc::new(data);
let _ = self.event_channel.unbounded_send(UiEvent {
name: "mounted".to_string(),
bubbles: false,
element: id,
data,
});
}
self.send_mount_event(id);
}
}
pub(crate) fn send_mount_event(&self, id: ElementId) {
let node = get_node(id.0 as u32);
if let Some(element) = node.dyn_ref::<Element>() {
let data: MountedData = element.into();
let data = Rc::new(data);
let _ = self.event_channel.unbounded_send(UiEvent {
name: "mounted".to_string(),
bubbles: false,
element: id,
data,
});
}
}
}
+14 -5
View File
@@ -125,6 +125,7 @@ impl WebsysDom {
children, attrs, ..
} => {
let mut mounted_id = None;
let mut should_send_mount_event = true;
for attr in *attrs {
if let dioxus_core::TemplateAttribute::Dynamic { id } = attr {
let attribute = &vnode.dynamic_attrs[*id];
@@ -134,16 +135,24 @@ impl WebsysDom {
let name = attribute.name;
if let AttributeValue::Listener(_) = value {
let event_name = &name[2..];
self.interpreter.new_event_listener(
event_name,
id.0 as u32,
event_bubbles(event_name) as u8,
);
match event_name {
"mounted" => should_send_mount_event = true,
_ => {
self.interpreter.new_event_listener(
event_name,
id.0 as u32,
event_bubbles(event_name) as u8,
);
}
}
}
}
}
if let Some(id) = mounted_id {
set_node(hydrated, id, current_child.clone()?);
if should_send_mount_event {
self.send_mount_event(id);
}
}
if !children.is_empty() {
let mut children_current_child = current_child