Merge branch 'master' into binary-protocal
This commit is contained in:
76 files changed
+1918
-625
No files matched your search
@@ -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
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
/dist
|
||||
Cargo.lock
|
||||
.DS_Store
|
||||
/examples/assets/test_video.mp4
|
||||
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
|
||||
@@ -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"
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
@@ -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" }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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())),
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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?
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
///
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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,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(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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,
|
||||
));
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = []
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
///
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"]
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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 ),* ]),
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 `"/"`.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -238,6 +238,94 @@ fn to_string_works() {
|
||||
assert_eq!(out, "<div class=\"asdasdasd\" class=\"asdasdasd\" id=\"id-123\">Hello world 1 -->123<-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div></diiiiiiiiv><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",
|
||||
|
||||
@@ -42,6 +42,7 @@ features = [
|
||||
"HtmlFormElement",
|
||||
"Text",
|
||||
"Window",
|
||||
"console",
|
||||
]
|
||||
|
||||
[features]
|
||||
|
||||
@@ -190,7 +190,7 @@ pub static BUILTIN_INTERNED_STRINGS: &[&str] = &[
|
||||
"oncopy",
|
||||
"oncuechange",
|
||||
"oncut",
|
||||
"ondblclick",
|
||||
"ondoubleclick",
|
||||
"ondrag",
|
||||
"ondragend",
|
||||
"ondragenter",
|
||||
|
||||
+25
-12
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user