Merge branch 'master' into bump-wry
This commit is contained in:
46 files changed
+1083
-190
No files matched your search
@@ -4,6 +4,7 @@
|
|||||||
/dist
|
/dist
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
/examples/assets/test_video.mp4
|
||||||
|
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/settings.json
|
!.vscode/settings.json
|
||||||
|
|||||||
@@ -133,3 +133,4 @@ fern = { version = "0.6.0", features = ["colored"] }
|
|||||||
env_logger = "0.10.0"
|
env_logger = "0.10.0"
|
||||||
simple_logger = "4.0.0"
|
simple_logger = "4.0.0"
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
http-range = "0.1.5"
|
||||||
@@ -53,8 +53,7 @@ fn app(cx: Scope) -> Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
cx.render(rsx! (
|
cx.render(rsx! (
|
||||||
div {
|
div { style: "{CONTAINER_STYLE}",
|
||||||
style: "{CONTAINER_STYLE}",
|
|
||||||
div {
|
div {
|
||||||
style: "{RECT_STYLE}",
|
style: "{RECT_STYLE}",
|
||||||
// focusing is necessary to catch keyboard events
|
// focusing is necessary to catch keyboard events
|
||||||
@@ -62,7 +61,7 @@ fn app(cx: Scope) -> Element {
|
|||||||
|
|
||||||
onmousemove: move |event| log_event(Event::MouseMove(event)),
|
onmousemove: move |event| log_event(Event::MouseMove(event)),
|
||||||
onclick: move |event| log_event(Event::MouseClick(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)),
|
onmousedown: move |event| log_event(Event::MouseDown(event)),
|
||||||
onmouseup: move |event| log_event(Event::MouseUp(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"
|
"Hover, click, type or scroll to see the info down below"
|
||||||
}
|
}
|
||||||
div {
|
div { events.read().iter().map(|event| rsx!( div { "{event:?}" } )) }
|
||||||
events.read().iter().map(|event| rsx!( div { "{event:?}" } ))
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
+48
-41
@@ -48,11 +48,8 @@ pub fn app(cx: Scope<()>) -> Element {
|
|||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
section { class: "todoapp",
|
section { class: "todoapp",
|
||||||
style { include_str!("./assets/todomvc.css") }
|
style { include_str!("./assets/todomvc.css") }
|
||||||
TodoHeader {
|
TodoHeader { todos: todos }
|
||||||
todos: todos,
|
section { class: "main",
|
||||||
}
|
|
||||||
section {
|
|
||||||
class: "main",
|
|
||||||
if !todos.is_empty() {
|
if !todos.is_empty() {
|
||||||
rsx! {
|
rsx! {
|
||||||
input {
|
input {
|
||||||
@@ -103,31 +100,34 @@ pub fn TodoHeader<'a>(cx: Scope<'a, TodoHeaderProps<'a>>) -> Element {
|
|||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
header { class: "header",
|
header { class: "header",
|
||||||
h1 {"todos"}
|
h1 { "todos" }
|
||||||
input {
|
input {
|
||||||
class: "new-todo",
|
class: "new-todo",
|
||||||
placeholder: "What needs to be done?",
|
placeholder: "What needs to be done?",
|
||||||
value: "{draft}",
|
value: "{draft}",
|
||||||
autofocus: "true",
|
autofocus: "true",
|
||||||
oninput: move |evt| {
|
oninput: move |evt| {
|
||||||
draft.set(evt.value.clone());
|
draft.set(evt.value.clone());
|
||||||
},
|
},
|
||||||
onkeydown: move |evt| {
|
onkeydown: move |evt| {
|
||||||
if evt.key() == Key::Enter && !draft.is_empty() {
|
if evt.key() == Key::Enter && !draft.is_empty() {
|
||||||
cx.props.todos.make_mut().insert(
|
cx.props
|
||||||
**todo_id,
|
.todos
|
||||||
TodoItem {
|
.make_mut()
|
||||||
id: **todo_id,
|
.insert(
|
||||||
checked: false,
|
**todo_id,
|
||||||
contents: draft.to_string(),
|
TodoItem {
|
||||||
},
|
id: **todo_id,
|
||||||
);
|
checked: false,
|
||||||
*todo_id.make_mut() += 1;
|
contents: draft.to_string(),
|
||||||
draft.set("".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 { "" };
|
let editing = if **is_editing { "editing" } else { "" };
|
||||||
|
|
||||||
cx.render(rsx!{
|
cx.render(rsx!{
|
||||||
li {
|
li { class: "{completed} {editing}",
|
||||||
class: "{completed} {editing}",
|
|
||||||
div { class: "view",
|
div { class: "view",
|
||||||
input {
|
input {
|
||||||
class: "toggle",
|
class: "toggle",
|
||||||
@@ -160,14 +159,16 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
|
|||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
r#for: "cbg-{todo.id}",
|
r#for: "cbg-{todo.id}",
|
||||||
ondblclick: move |_| is_editing.set(true),
|
ondoubleclick: move |_| is_editing.set(true),
|
||||||
prevent_default: "onclick",
|
prevent_default: "onclick",
|
||||||
"{todo.contents}"
|
"{todo.contents}"
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
class: "destroy",
|
class: "destroy",
|
||||||
onclick: move |_| { cx.props.todos.make_mut().remove(&todo.id); },
|
onclick: move |_| {
|
||||||
prevent_default: "onclick",
|
cx.props.todos.make_mut().remove(&todo.id);
|
||||||
|
},
|
||||||
|
prevent_default: "onclick"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is_editing.then(|| rsx!{
|
is_editing.then(|| rsx!{
|
||||||
@@ -213,15 +214,15 @@ pub fn ListFooter<'a>(cx: Scope<'a, ListFooterProps<'a>>) -> Element {
|
|||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
footer { class: "footer",
|
footer { class: "footer",
|
||||||
span { class: "todo-count",
|
span { class: "todo-count",
|
||||||
strong {"{active_todo_count} "}
|
strong { "{active_todo_count} " }
|
||||||
span {"{active_todo_text} left"}
|
span { "{active_todo_text} left" }
|
||||||
}
|
}
|
||||||
ul { class: "filters",
|
ul { class: "filters",
|
||||||
for (state, state_text, url) in [
|
for (state , state_text , url) in [
|
||||||
(FilterState::All, "All", "#/"),
|
(FilterState::All, "All", "#/"),
|
||||||
(FilterState::Active, "Active", "#/active"),
|
(FilterState::Active, "Active", "#/active"),
|
||||||
(FilterState::Completed, "Completed", "#/completed"),
|
(FilterState::Completed, "Completed", "#/completed"),
|
||||||
] {
|
] {
|
||||||
li {
|
li {
|
||||||
a {
|
a {
|
||||||
href: url,
|
href: url,
|
||||||
@@ -250,8 +251,14 @@ pub fn PageFooter(cx: Scope) -> Element {
|
|||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
footer { class: "info",
|
footer { class: "info",
|
||||||
p { "Double-click to edit a todo" }
|
p { "Double-click to edit a todo" }
|
||||||
p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }}
|
p {
|
||||||
p { "Part of ", a { href: "http://todomvc.com", "TodoMVC" }}
|
"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)
|
||||||
|
}
|
||||||
@@ -93,6 +93,8 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
|
|||||||
cmd
|
cmd
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let cmd = cmd.args(&config.cargo_args);
|
||||||
|
|
||||||
let cmd = match executable {
|
let cmd = match executable {
|
||||||
ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
|
ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
|
||||||
ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
|
ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
|
||||||
@@ -286,6 +288,14 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
|
|||||||
cmd = cmd.arg("--features").arg(features_str);
|
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 {
|
let cmd = match &config.executable {
|
||||||
crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
|
crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
|
||||||
crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
|
crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
|
||||||
@@ -303,12 +313,17 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
|
|||||||
let mut res_path = match &config.executable {
|
let mut res_path = match &config.executable {
|
||||||
crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => {
|
crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => {
|
||||||
file_name = name.clone();
|
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) => {
|
crate::ExecutableType::Example(name) => {
|
||||||
file_name = name.clone();
|
file_name = name.clone();
|
||||||
config
|
config
|
||||||
.target_dir
|
.target_dir
|
||||||
|
.join(target_platform)
|
||||||
.join(release_type)
|
.join(release_type)
|
||||||
.join("examples")
|
.join("examples")
|
||||||
.join(name)
|
.join(name)
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ impl Build {
|
|||||||
.platform
|
.platform
|
||||||
.unwrap_or(crate_config.dioxus_config.application.default_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")]
|
// #[cfg(feature = "plugin")]
|
||||||
// let _ = PluginManager::on_build_start(&crate_config, &platform);
|
// let _ = PluginManager::on_build_start(&crate_config, &platform);
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,12 @@ impl Bundle {
|
|||||||
crate_config.set_profile(self.build.profile.unwrap());
|
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 the desktop app
|
||||||
build_desktop(&crate_config, false)?;
|
build_desktop(&crate_config, false)?;
|
||||||
|
|
||||||
@@ -148,6 +154,11 @@ impl Bundle {
|
|||||||
.collect(),
|
.collect(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(target) = &self.build.target {
|
||||||
|
settings = settings.target(target.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
let settings = settings.build();
|
let settings = settings.build();
|
||||||
|
|
||||||
// on macos we need to set CI=true (https://github.com/tauri-apps/tauri/issues/2567)
|
// 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|{
|
tauri_bundler::bundle::bundle_project(settings.unwrap()).unwrap_or_else(|err|{
|
||||||
#[cfg(target_os = "macos")]
|
#[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"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
panic!("Failed to bundle project: {}", err);
|
panic!("Failed to bundle project: {:#?}", err);
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -6,10 +6,6 @@ use super::*;
|
|||||||
/// Config options for the build system.
|
/// Config options for the build system.
|
||||||
#[derive(Clone, Debug, Default, Deserialize, Parser)]
|
#[derive(Clone, Debug, Default, Deserialize, Parser)]
|
||||||
pub struct ConfigOptsBuild {
|
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]
|
/// Build in release mode [default: false]
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -35,14 +31,18 @@ pub struct ConfigOptsBuild {
|
|||||||
/// Space separated list of features to activate
|
/// Space separated list of features to activate
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
pub features: Option<Vec<String>>,
|
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)]
|
#[derive(Clone, Debug, Default, Deserialize, Parser)]
|
||||||
pub struct ConfigOptsServe {
|
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
|
/// Port of dev server
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
#[clap(default_value_t = 8080)]
|
#[clap(default_value_t = 8080)]
|
||||||
@@ -89,6 +89,14 @@ pub struct ConfigOptsServe {
|
|||||||
/// Space separated list of features to activate
|
/// Space separated list of features to activate
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
pub features: Option<Vec<String>>,
|
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)]
|
#[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
|
/// Space separated list of features to activate
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
pub features: Option<Vec<String>>,
|
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
|
// Subdirectories don't work with the server
|
||||||
crate_config.dioxus_config.web.app.base_path = None;
|
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
|
let platform = self
|
||||||
.serve
|
.serve
|
||||||
.platform
|
.platform
|
||||||
|
|||||||
@@ -211,6 +211,8 @@ pub struct CrateConfig {
|
|||||||
pub verbose: bool,
|
pub verbose: bool,
|
||||||
pub custom_profile: Option<String>,
|
pub custom_profile: Option<String>,
|
||||||
pub features: Option<Vec<String>>,
|
pub features: Option<Vec<String>>,
|
||||||
|
pub target: Option<String>,
|
||||||
|
pub cargo_args: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -278,6 +280,8 @@ impl CrateConfig {
|
|||||||
let verbose = false;
|
let verbose = false;
|
||||||
let custom_profile = None;
|
let custom_profile = None;
|
||||||
let features = None;
|
let features = None;
|
||||||
|
let target = None;
|
||||||
|
let cargo_args = vec![];
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
out_dir,
|
out_dir,
|
||||||
@@ -294,6 +298,8 @@ impl CrateConfig {
|
|||||||
custom_profile,
|
custom_profile,
|
||||||
features,
|
features,
|
||||||
verbose,
|
verbose,
|
||||||
|
target,
|
||||||
|
cargo_args,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,6 +337,16 @@ impl CrateConfig {
|
|||||||
self.features = Some(features);
|
self.features = Some(features);
|
||||||
self
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use axum::{
|
|||||||
body::{Full, HttpBody},
|
body::{Full, HttpBody},
|
||||||
extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
|
extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
|
||||||
http::{
|
http::{
|
||||||
|
self,
|
||||||
header::{HeaderName, HeaderValue},
|
header::{HeaderName, HeaderValue},
|
||||||
Method, Response, StatusCode,
|
Method, Response, StatusCode,
|
||||||
},
|
},
|
||||||
@@ -262,7 +263,7 @@ async fn setup_router(
|
|||||||
.override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
|
.override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
|
||||||
.and_then(
|
.and_then(
|
||||||
move |response: Response<ServeFileSystemResponseBody>| async move {
|
move |response: Response<ServeFileSystemResponseBody>| async move {
|
||||||
let response = if file_service_config
|
let mut response = if file_service_config
|
||||||
.dioxus_config
|
.dioxus_config
|
||||||
.web
|
.web
|
||||||
.watcher
|
.watcher
|
||||||
@@ -290,6 +291,13 @@ async fn setup_router(
|
|||||||
} else {
|
} else {
|
||||||
response.map(|body| body.boxed())
|
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)
|
Ok(response)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -58,8 +58,11 @@ impl ToTokens for ComponentDeserializerOutput {
|
|||||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||||
let comp_fn = &self.comp_fn;
|
let comp_fn = &self.comp_fn;
|
||||||
let props_struct = &self.props_struct;
|
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! {
|
tokens.append_all(quote! {
|
||||||
|
#[doc = #doc]
|
||||||
#props_struct
|
#props_struct
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
#comp_fn
|
#comp_fn
|
||||||
|
|||||||
@@ -205,7 +205,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
|
/// Elements can contain other nodes - and those nodes can be dynamic or static
|
||||||
///
|
///
|
||||||
@@ -405,6 +405,7 @@ impl<'b> VirtualDom {
|
|||||||
#[allow(unused_mut)]
|
#[allow(unused_mut)]
|
||||||
pub(crate) fn register_template(&mut self, mut template: Template<'static>) {
|
pub(crate) fn register_template(&mut self, mut template: Template<'static>) {
|
||||||
let (path, byte_index) = template.name.rsplit_once(':').unwrap();
|
let (path, byte_index) = template.name.rsplit_once(':').unwrap();
|
||||||
|
|
||||||
let byte_index = byte_index.parse::<usize>().unwrap();
|
let byte_index = byte_index.parse::<usize>().unwrap();
|
||||||
// First, check if we've already seen this template
|
// First, check if we've already seen this template
|
||||||
if self
|
if self
|
||||||
|
|||||||
@@ -230,17 +230,7 @@ impl ScopeContext {
|
|||||||
/// This is good for tasks that need to be run after the component has been dropped.
|
/// 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 {
|
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
|
// 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);
|
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Informs the scheduler that this task is no longer needed and should be removed.
|
/// Informs the scheduler that this task is no longer needed and should be removed.
|
||||||
|
|||||||
@@ -229,75 +229,111 @@ fn app(cx: Scope) -> Element {
|
|||||||
println!("{:?}", event.data);
|
println!("{:?}", event.data);
|
||||||
assert!(event.data.modifiers().is_empty());
|
assert!(event.data.modifiers().is_empty());
|
||||||
assert!(event.data.held_buttons().is_empty());
|
assert!(event.data.held_buttons().is_empty());
|
||||||
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary));
|
assert_eq!(
|
||||||
|
event.data.trigger_button(),
|
||||||
|
Some(dioxus_html::input_data::MouseButton::Primary),
|
||||||
|
);
|
||||||
recieved_events.modify(|x| *x + 1)
|
recieved_events.modify(|x| *x + 1)
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
id: "mouse_move_div",
|
id: "mouse_move_div",
|
||||||
onmousemove: move |event| {
|
onmousemove: move |event| {
|
||||||
println!("{:?}", event.data);
|
println!("{:?}", event.data);
|
||||||
assert!(event.data.modifiers().is_empty());
|
assert!(event.data.modifiers().is_empty());
|
||||||
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
|
assert!(
|
||||||
|
event
|
||||||
|
.data
|
||||||
|
.held_buttons()
|
||||||
|
.contains(dioxus_html::input_data::MouseButton::Secondary),
|
||||||
|
);
|
||||||
recieved_events.modify(|x| *x + 1)
|
recieved_events.modify(|x| *x + 1)
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
id: "mouse_click_div",
|
id: "mouse_click_div",
|
||||||
onclick: move |event| {
|
onclick: move |event| {
|
||||||
println!("{:?}", event.data);
|
println!("{:?}", event.data);
|
||||||
assert!(event.data.modifiers().is_empty());
|
assert!(event.data.modifiers().is_empty());
|
||||||
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
|
assert!(
|
||||||
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
|
event
|
||||||
recieved_events.modify(|x| *x + 1)
|
.data
|
||||||
},
|
.held_buttons()
|
||||||
}
|
.contains(dioxus_html::input_data::MouseButton::Secondary),
|
||||||
div{
|
);
|
||||||
id: "mouse_dblclick_div",
|
assert_eq!(
|
||||||
ondblclick: move |event| {
|
event.data.trigger_button(),
|
||||||
println!("{:?}", event.data);
|
Some(dioxus_html::input_data::MouseButton::Secondary),
|
||||||
assert!(event.data.modifiers().is_empty());
|
);
|
||||||
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary));
|
|
||||||
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));
|
|
||||||
recieved_events.modify(|x| *x + 1)
|
recieved_events.modify(|x| *x + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div{
|
div {
|
||||||
|
id: "mouse_dblclick_div",
|
||||||
|
ondoubleclick: move |event| {
|
||||||
|
println!("{:?}", event.data);
|
||||||
|
assert!(event.data.modifiers().is_empty());
|
||||||
|
assert!(
|
||||||
|
event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary),
|
||||||
|
);
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
recieved_events.modify(|x| *x + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
id: "mouse_down_div",
|
id: "mouse_down_div",
|
||||||
onmousedown: move |event| {
|
onmousedown: move |event| {
|
||||||
println!("{:?}", event.data);
|
println!("{:?}", event.data);
|
||||||
assert!(event.data.modifiers().is_empty());
|
assert!(event.data.modifiers().is_empty());
|
||||||
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
|
assert!(
|
||||||
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
|
event
|
||||||
|
.data
|
||||||
|
.held_buttons()
|
||||||
|
.contains(dioxus_html::input_data::MouseButton::Secondary),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
event.data.trigger_button(),
|
||||||
|
Some(dioxus_html::input_data::MouseButton::Secondary),
|
||||||
|
);
|
||||||
recieved_events.modify(|x| *x + 1)
|
recieved_events.modify(|x| *x + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div{
|
div {
|
||||||
id: "mouse_up_div",
|
id: "mouse_up_div",
|
||||||
onmouseup: move |event| {
|
onmouseup: move |event| {
|
||||||
println!("{:?}", event.data);
|
println!("{:?}", event.data);
|
||||||
assert!(event.data.modifiers().is_empty());
|
assert!(event.data.modifiers().is_empty());
|
||||||
assert!(event.data.held_buttons().is_empty());
|
assert!(event.data.held_buttons().is_empty());
|
||||||
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary));
|
assert_eq!(
|
||||||
|
event.data.trigger_button(),
|
||||||
|
Some(dioxus_html::input_data::MouseButton::Primary),
|
||||||
|
);
|
||||||
recieved_events.modify(|x| *x + 1)
|
recieved_events.modify(|x| *x + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div{
|
div {
|
||||||
id: "wheel_div",
|
id: "wheel_div",
|
||||||
width: "100px",
|
width: "100px",
|
||||||
height: "100px",
|
height: "100px",
|
||||||
background_color: "red",
|
background_color: "red",
|
||||||
onwheel: move |event| {
|
onwheel: move |event| {
|
||||||
println!("{:?}", event.data);
|
println!("{:?}", event.data);
|
||||||
let dioxus_html::geometry::WheelDelta::Pixels(delta)= event.data.delta()else{
|
let dioxus_html::geometry::WheelDelta::Pixels(delta) = event.data.delta() else {
|
||||||
panic!("Expected delta to be in pixels")
|
panic!("Expected delta to be in pixels") };
|
||||||
};
|
|
||||||
assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0));
|
assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0));
|
||||||
recieved_events.modify(|x| *x + 1)
|
recieved_events.modify(|x| *x + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
input{
|
input {
|
||||||
id: "key_down_div",
|
id: "key_down_div",
|
||||||
onkeydown: move |event| {
|
onkeydown: move |event| {
|
||||||
println!("{:?}", event.data);
|
println!("{:?}", event.data);
|
||||||
@@ -306,11 +342,10 @@ fn app(cx: Scope) -> Element {
|
|||||||
assert_eq!(event.data.code().to_string(), "KeyA");
|
assert_eq!(event.data.code().to_string(), "KeyA");
|
||||||
assert_eq!(event.data.location, 0);
|
assert_eq!(event.data.location, 0);
|
||||||
assert!(event.data.is_auto_repeating());
|
assert!(event.data.is_auto_repeating());
|
||||||
|
|
||||||
recieved_events.modify(|x| *x + 1)
|
recieved_events.modify(|x| *x + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
input{
|
input {
|
||||||
id: "key_up_div",
|
id: "key_up_div",
|
||||||
onkeyup: move |event| {
|
onkeyup: move |event| {
|
||||||
println!("{:?}", event.data);
|
println!("{:?}", event.data);
|
||||||
@@ -319,11 +354,10 @@ fn app(cx: Scope) -> Element {
|
|||||||
assert_eq!(event.data.code().to_string(), "KeyA");
|
assert_eq!(event.data.code().to_string(), "KeyA");
|
||||||
assert_eq!(event.data.location, 0);
|
assert_eq!(event.data.location, 0);
|
||||||
assert!(!event.data.is_auto_repeating());
|
assert!(!event.data.is_auto_repeating());
|
||||||
|
|
||||||
recieved_events.modify(|x| *x + 1)
|
recieved_events.modify(|x| *x + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
input{
|
input {
|
||||||
id: "key_press_div",
|
id: "key_press_div",
|
||||||
onkeypress: move |event| {
|
onkeypress: move |event| {
|
||||||
println!("{:?}", event.data);
|
println!("{:?}", event.data);
|
||||||
@@ -332,18 +366,17 @@ fn app(cx: Scope) -> Element {
|
|||||||
assert_eq!(event.data.code().to_string(), "KeyA");
|
assert_eq!(event.data.code().to_string(), "KeyA");
|
||||||
assert_eq!(event.data.location, 0);
|
assert_eq!(event.data.location, 0);
|
||||||
assert!(!event.data.is_auto_repeating());
|
assert!(!event.data.is_auto_repeating());
|
||||||
|
|
||||||
recieved_events.modify(|x| *x + 1)
|
recieved_events.modify(|x| *x + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
input{
|
input {
|
||||||
id: "focus_in_div",
|
id: "focus_in_div",
|
||||||
onfocusin: move |event| {
|
onfocusin: move |event| {
|
||||||
println!("{:?}", event.data);
|
println!("{:?}", event.data);
|
||||||
recieved_events.modify(|x| *x + 1)
|
recieved_events.modify(|x| *x + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
input{
|
input {
|
||||||
id: "focus_out_div",
|
id: "focus_out_div",
|
||||||
onfocusout: move |event| {
|
onfocusout: move |event| {
|
||||||
println!("{:?}", event.data);
|
println!("{:?}", event.data);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ pub struct Config {
|
|||||||
pub(crate) root_name: String,
|
pub(crate) root_name: String,
|
||||||
pub(crate) background_color: Option<(u8, u8, u8, u8)>,
|
pub(crate) background_color: Option<(u8, u8, u8, u8)>,
|
||||||
pub(crate) last_window_close_behaviour: WindowCloseBehaviour,
|
pub(crate) last_window_close_behaviour: WindowCloseBehaviour,
|
||||||
|
pub(crate) enable_default_menu_bar: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>;
|
type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>;
|
||||||
@@ -65,9 +66,18 @@ impl Config {
|
|||||||
root_name: "main".to_string(),
|
root_name: "main".to_string(),
|
||||||
background_color: None,
|
background_color: None,
|
||||||
last_window_close_behaviour: WindowCloseBehaviour::LastWindowExitsApp,
|
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
|
/// set the directory from which assets will be searched in release mode
|
||||||
pub fn with_resource_directory(mut self, path: impl Into<PathBuf>) -> Self {
|
pub fn with_resource_directory(mut self, path: impl Into<PathBuf>) -> Self {
|
||||||
self.resource_dir = Some(path.into());
|
self.resource_dir = Some(path.into());
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ use std::rc::Weak;
|
|||||||
|
|
||||||
use crate::create_new_window;
|
use crate::create_new_window;
|
||||||
use crate::events::IpcMessage;
|
use crate::events::IpcMessage;
|
||||||
|
use crate::protocol::AssetFuture;
|
||||||
|
use crate::protocol::AssetHandlerRegistry;
|
||||||
use crate::query::QueryEngine;
|
use crate::query::QueryEngine;
|
||||||
use crate::shortcut::{HotKey, ShortcutId, ShortcutRegistry, ShortcutRegistryError};
|
use crate::shortcut::{HotKey, ShortcutId, ShortcutRegistry, ShortcutRegistryError};
|
||||||
|
use crate::AssetHandler;
|
||||||
use crate::Config;
|
use crate::Config;
|
||||||
use crate::WebviewHandler;
|
use crate::WebviewHandler;
|
||||||
use dioxus_core::ScopeState;
|
use dioxus_core::ScopeState;
|
||||||
@@ -64,6 +67,8 @@ pub struct DesktopService {
|
|||||||
|
|
||||||
pub(crate) shortcut_manager: ShortcutRegistry,
|
pub(crate) shortcut_manager: ShortcutRegistry,
|
||||||
|
|
||||||
|
pub(crate) asset_handlers: AssetHandlerRegistry,
|
||||||
|
|
||||||
#[cfg(target_os = "ios")]
|
#[cfg(target_os = "ios")]
|
||||||
pub(crate) views: Rc<RefCell<Vec<*mut objc::runtime::Object>>>,
|
pub(crate) views: Rc<RefCell<Vec<*mut objc::runtime::Object>>>,
|
||||||
}
|
}
|
||||||
@@ -88,6 +93,7 @@ impl DesktopService {
|
|||||||
webviews: WebviewQueue,
|
webviews: WebviewQueue,
|
||||||
event_handlers: WindowEventHandlers,
|
event_handlers: WindowEventHandlers,
|
||||||
shortcut_manager: ShortcutRegistry,
|
shortcut_manager: ShortcutRegistry,
|
||||||
|
asset_handlers: AssetHandlerRegistry,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
webview: Rc::new(webview),
|
webview: Rc::new(webview),
|
||||||
@@ -97,6 +103,7 @@ impl DesktopService {
|
|||||||
pending_windows: webviews,
|
pending_windows: webviews,
|
||||||
event_handlers,
|
event_handlers,
|
||||||
shortcut_manager,
|
shortcut_manager,
|
||||||
|
asset_handlers,
|
||||||
#[cfg(target_os = "ios")]
|
#[cfg(target_os = "ios")]
|
||||||
views: Default::default(),
|
views: Default::default(),
|
||||||
}
|
}
|
||||||
@@ -247,6 +254,20 @@ impl DesktopService {
|
|||||||
self.shortcut_manager.remove_all()
|
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
|
/// Push an objc view to the window
|
||||||
#[cfg(target_os = "ios")]
|
#[cfg(target_os = "ios")]
|
||||||
pub fn push_view(&self, view: objc_id::ShareId<objc::runtime::Object>) {
|
pub fn push_view(&self, view: objc_id::ShareId<objc::runtime::Object>) {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent};
|
|||||||
use element::DesktopElement;
|
use element::DesktopElement;
|
||||||
use eval::init_eval;
|
use eval::init_eval;
|
||||||
use futures_util::{pin_mut, FutureExt};
|
use futures_util::{pin_mut, FutureExt};
|
||||||
|
pub use protocol::{use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse};
|
||||||
use shortcut::ShortcutRegistry;
|
use shortcut::ShortcutRegistry;
|
||||||
pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
|
pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
@@ -45,6 +46,7 @@ use tao::{
|
|||||||
event::{Event, StartCause, WindowEvent},
|
event::{Event, StartCause, WindowEvent},
|
||||||
event_loop::ControlFlow,
|
event_loop::ControlFlow,
|
||||||
};
|
};
|
||||||
|
pub use webview::build_default_menu_bar;
|
||||||
pub use wry;
|
pub use wry;
|
||||||
pub use wry::application as tao;
|
pub use wry::application as tao;
|
||||||
use wry::application::event_loop::EventLoopBuilder;
|
use wry::application::event_loop::EventLoopBuilder;
|
||||||
@@ -395,7 +397,8 @@ fn create_new_window(
|
|||||||
event_handlers: &WindowEventHandlers,
|
event_handlers: &WindowEventHandlers,
|
||||||
shortcut_manager: ShortcutRegistry,
|
shortcut_manager: ShortcutRegistry,
|
||||||
) -> WebviewHandler {
|
) -> WebviewHandler {
|
||||||
let (webview, web_context) = webview::build(&mut cfg, event_loop, proxy.clone());
|
let (webview, web_context, asset_handlers) =
|
||||||
|
webview::build(&mut cfg, event_loop, proxy.clone());
|
||||||
let desktop_context = Rc::from(DesktopService::new(
|
let desktop_context = Rc::from(DesktopService::new(
|
||||||
webview,
|
webview,
|
||||||
proxy.clone(),
|
proxy.clone(),
|
||||||
@@ -403,6 +406,7 @@ fn create_new_window(
|
|||||||
queue.clone(),
|
queue.clone(),
|
||||||
event_handlers.clone(),
|
event_handlers.clone(),
|
||||||
shortcut_manager,
|
shortcut_manager,
|
||||||
|
asset_handlers,
|
||||||
));
|
));
|
||||||
|
|
||||||
let cx = dom.base_scope();
|
let cx = dom.base_scope();
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
|
use dioxus_core::ScopeState;
|
||||||
use dioxus_interpreter_js::{COMMON_JS, INTERPRETER_JS};
|
use dioxus_interpreter_js::{COMMON_JS, INTERPRETER_JS};
|
||||||
|
use slab::Slab;
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
|
future::Future,
|
||||||
|
ops::Deref,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
pin::Pin,
|
||||||
|
rc::Rc,
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
use tokio::{
|
||||||
|
runtime::Handle,
|
||||||
|
sync::{OnceCell, RwLock},
|
||||||
};
|
};
|
||||||
use wry::{
|
use wry::{
|
||||||
http::{status::StatusCode, Request, Response},
|
http::{status::StatusCode, Request, Response},
|
||||||
Result,
|
Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::{use_window, DesktopContext};
|
||||||
|
|
||||||
fn module_loader(root_name: &str) -> String {
|
fn module_loader(root_name: &str) -> String {
|
||||||
let js = INTERPRETER_JS.replace(
|
let js = INTERPRETER_JS.replace(
|
||||||
"/*POST_HANDLE_EDITS*/",
|
"/*POST_HANDLE_EDITS*/",
|
||||||
@@ -51,12 +64,156 @@ fn module_loader(root_name: &str) -> String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn desktop_handler(
|
/// An arbitrary asset is an HTTP response containing a binary body.
|
||||||
request: &Request<Vec<u8>>,
|
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_head: Option<String>,
|
||||||
custom_index: Option<String>,
|
custom_index: Option<String>,
|
||||||
root_name: &str,
|
root_name: &str,
|
||||||
) -> Result<Response<Cow<'static, [u8]>>> {
|
asset_handlers: &AssetHandlerRegistry,
|
||||||
|
) -> Result<AssetResponse> {
|
||||||
|
let request = AssetRequest::from(request);
|
||||||
|
|
||||||
// If the request is for the root, we'll serve the index.html file.
|
// If the request is for the root, we'll serve the index.html file.
|
||||||
if request.uri().path() == "/" {
|
if request.uri().path() == "/" {
|
||||||
// If a custom index is provided, just defer to that, expecting the user to know what they're doing.
|
// If a custom index is provided, just defer to that, expecting the user to know what they're doing.
|
||||||
@@ -91,18 +248,21 @@ pub(super) fn desktop_handler(
|
|||||||
.map_err(From::from);
|
.map_err(From::from);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
// 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.
|
// If the path is relative, we'll try to serve it from the assets directory.
|
||||||
let mut asset = get_asset_root()
|
let mut asset = get_asset_root()
|
||||||
.unwrap_or_else(|| Path::new(".").to_path_buf())
|
.unwrap_or_else(|| Path::new(".").to_path_buf())
|
||||||
.join(&path);
|
.join(&request.path);
|
||||||
|
|
||||||
if !asset.exists() {
|
if !asset.exists() {
|
||||||
asset = PathBuf::from("/").join(path);
|
asset = PathBuf::from("/").join(request.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
if asset.exists() {
|
if asset.exists() {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use crate::desktop_context::EventData;
|
use crate::desktop_context::EventData;
|
||||||
use crate::protocol;
|
use crate::protocol::{self, AssetHandlerRegistry};
|
||||||
use crate::{desktop_context::UserWindowEvent, Config};
|
use crate::{desktop_context::UserWindowEvent, Config};
|
||||||
use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
|
use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
|
||||||
pub use wry;
|
pub use wry;
|
||||||
pub use wry::application as tao;
|
pub use wry::application as tao;
|
||||||
|
use wry::application::menu::{MenuBar, MenuItem};
|
||||||
use wry::application::window::Window;
|
use wry::application::window::Window;
|
||||||
use wry::http::Response;
|
use wry::http::Response;
|
||||||
use wry::webview::{WebContext, WebView, WebViewBuilder};
|
use wry::webview::{WebContext, WebView, WebViewBuilder};
|
||||||
@@ -12,14 +13,19 @@ pub fn build(
|
|||||||
cfg: &mut Config,
|
cfg: &mut Config,
|
||||||
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
|
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
|
||||||
proxy: EventLoopProxy<UserWindowEvent>,
|
proxy: EventLoopProxy<UserWindowEvent>,
|
||||||
) -> (WebView, WebContext) {
|
) -> (WebView, WebContext, AssetHandlerRegistry) {
|
||||||
let builder = cfg.window.clone();
|
|
||||||
let window = builder.with_visible(false).build(event_loop).unwrap();
|
let window = builder.with_visible(false).build(event_loop).unwrap();
|
||||||
let file_handler = cfg.file_drop_handler.take();
|
let file_handler = cfg.file_drop_handler.take();
|
||||||
let custom_head = cfg.custom_head.clone();
|
let custom_head = cfg.custom_head.clone();
|
||||||
let index_file = cfg.custom_index.clone();
|
let index_file = cfg.custom_index.clone();
|
||||||
let root_name = cfg.root_name.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
|
// 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() {
|
if cfg.window.window.window_icon.is_none() {
|
||||||
window.set_window_icon(Some(
|
window.set_window_icon(Some(
|
||||||
@@ -33,6 +39,8 @@ pub fn build(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut web_context = WebContext::new(cfg.data_dir.clone());
|
let mut web_context = WebContext::new(cfg.data_dir.clone());
|
||||||
|
let asset_handlers = AssetHandlerRegistry::new();
|
||||||
|
let asset_handlers_ref = asset_handlers.clone();
|
||||||
|
|
||||||
let mut webview = WebViewBuilder::new(window)
|
let mut webview = WebViewBuilder::new(window)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -45,24 +53,23 @@ pub fn build(
|
|||||||
_ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window.id()));
|
_ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window.id()));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.with_custom_protocol(
|
.with_asynchronous_custom_protocol(String::from("dioxus"), move |request, responder| {
|
||||||
String::from("dioxus"),
|
let custom_head = custom_head.clone();
|
||||||
move |r| match protocol::desktop_handler(
|
let index_file = index_file.clone();
|
||||||
&r,
|
let root_name = root_name.clone();
|
||||||
custom_head.clone(),
|
let asset_handlers_ref = asset_handlers_ref.clone();
|
||||||
index_file.clone(),
|
tokio::spawn(async move {
|
||||||
&root_name,
|
let response_res = protocol::desktop_handler(
|
||||||
) {
|
request,
|
||||||
Ok(response) => response,
|
custom_head.clone(),
|
||||||
Err(err) => {
|
index_file.clone(),
|
||||||
tracing::error!("Error: {}", err);
|
&root_name,
|
||||||
Response::builder()
|
&asset_handlers_ref,
|
||||||
.status(500)
|
)
|
||||||
.body(err.to_string().into_bytes().into())
|
.await;
|
||||||
.unwrap()
|
responder.respond(response);
|
||||||
}
|
});
|
||||||
},
|
})
|
||||||
)
|
|
||||||
.with_file_drop_handler(move |window, evet| {
|
.with_file_drop_handler(move |window, evet| {
|
||||||
file_handler
|
file_handler
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -119,5 +126,63 @@ pub fn build(
|
|||||||
webview = webview.with_devtools(true);
|
webview = webview.with_devtools(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
(webview.build().unwrap(), web_context)
|
(webview.build().unwrap(), web_context, asset_handlers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
// todo: remove
|
||||||
let mut trimmed = format!("{event:?}");
|
let mut trimmed = format!("{event:?}");
|
||||||
trimmed.truncate(200);
|
trimmed.truncate(200);
|
||||||
rsx!(p { "{trimmed}" })
|
rsx!( p { "{trimmed}" } )
|
||||||
});
|
});
|
||||||
|
|
||||||
let log_event = move |event: Event| {
|
let log_event = move |event: Event| {
|
||||||
@@ -45,10 +45,7 @@ fn app(cx: Scope) -> Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div { width: "100%", height: "100%", flex_direction: "column",
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
flex_direction: "column",
|
|
||||||
div {
|
div {
|
||||||
width: "80%",
|
width: "80%",
|
||||||
height: "50%",
|
height: "50%",
|
||||||
@@ -59,7 +56,7 @@ fn app(cx: Scope) -> Element {
|
|||||||
|
|
||||||
onmousemove: move |event| log_event(Event::MouseMove(event.inner().clone())),
|
onmousemove: move |event| log_event(Event::MouseMove(event.inner().clone())),
|
||||||
onclick: move |event| log_event(Event::MouseClick(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())),
|
onmousedown: move |event| log_event(Event::MouseDown(event.inner().clone())),
|
||||||
onmouseup: move |event| log_event(Event::MouseUp(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())),
|
onfocusout: move |event| log_event(Event::FocusOut(event.inner().clone())),
|
||||||
|
|
||||||
"Hover, click, type or scroll to see the info down below"
|
"Hover, click, type or scroll to see the info down below"
|
||||||
},
|
}
|
||||||
div {
|
div { width: "80%", height: "50%", flex_direction: "column", events_rendered }
|
||||||
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"
|
axum-macros = "0.3.7"
|
||||||
|
|
||||||
# salvo
|
# 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"
|
serde = "1.0.159"
|
||||||
|
|
||||||
# Dioxus + SSR
|
# Dioxus + SSR
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ dioxus = { workspace = true }
|
|||||||
dioxus-fullstack = { workspace = true }
|
dioxus-fullstack = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["full"], optional = true }
|
tokio = { workspace = true, features = ["full"], optional = true }
|
||||||
serde = "1.0.159"
|
serde = "1.0.159"
|
||||||
salvo = { version = "0.37.9", optional = true }
|
salvo = { version = "0.63.0", optional = true }
|
||||||
execute = "0.2.12"
|
execute = "0.2.12"
|
||||||
reqwest = "0.11.18"
|
reqwest = "0.11.18"
|
||||||
simple_logger = "4.2.0"
|
simple_logger = "4.2.0"
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ use serde::{de::DeserializeOwned, Serialize};
|
|||||||
/// use dioxus_fullstack::prelude::*;
|
/// use dioxus_fullstack::prelude::*;
|
||||||
///
|
///
|
||||||
/// fn app(cx: Scope) -> Element {
|
/// fn app(cx: Scope) -> Element {
|
||||||
/// let state1 = use_state(cx, || from_server(|| {
|
/// let state1 = use_state(cx, || server_cached(|| {
|
||||||
/// 1234
|
/// 1234
|
||||||
/// }));
|
/// }));
|
||||||
|
///
|
||||||
|
/// todo!()
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn server_cached<O: 'static + Serialize + DeserializeOwned>(server_fn: impl Fn() -> O) -> O {
|
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.
|
/// 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> {
|
pub fn invalid<T: 'static>(&self) -> GenerationalBox<T> {
|
||||||
let location = self.store.claim();
|
let location = self.store.claim();
|
||||||
GenerationalBox {
|
let key = GenerationalBox {
|
||||||
raw: location,
|
raw: location,
|
||||||
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||||
generation: location.0.generation.get(),
|
generation: location.0.generation.get(),
|
||||||
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
|
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
|
||||||
created_at: std::panic::Location::caller(),
|
created_at: std::panic::Location::caller(),
|
||||||
_marker: PhantomData,
|
_marker: PhantomData,
|
||||||
}
|
};
|
||||||
|
self.owned.borrow_mut().push(location);
|
||||||
|
key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,3 +68,4 @@ mounted = [
|
|||||||
wasm-bind = ["web-sys", "wasm-bindgen"]
|
wasm-bind = ["web-sys", "wasm-bindgen"]
|
||||||
native-bind = ["tokio"]
|
native-bind = ["tokio"]
|
||||||
hot-reload-context = ["dioxus-rsx"]
|
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 {
|
macro_rules! impl_element {
|
||||||
(
|
(
|
||||||
$(#[$attr:meta])*
|
$(#[$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!(
|
impl_element!(
|
||||||
$(#[$attr])*
|
$(#[$attr])*
|
||||||
@@ -998,9 +1049,8 @@ builder_constructors! {
|
|||||||
src: Uri DEFAULT,
|
src: Uri DEFAULT,
|
||||||
text: String DEFAULT,
|
text: String DEFAULT,
|
||||||
|
|
||||||
// r#async: Bool,
|
r#async: Bool "async",
|
||||||
// r#type: String, // TODO could be an enum
|
r#type: String "type", // TODO could be an enum
|
||||||
r#type: String "type",
|
|
||||||
r#script: String "script",
|
r#script: String "script",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -119,10 +119,7 @@ impl_event! {
|
|||||||
/// oncontextmenu
|
/// oncontextmenu
|
||||||
oncontextmenu
|
oncontextmenu
|
||||||
|
|
||||||
/// ondoubleclick
|
#[deprecated(since = "0.5.0", note = "use ondoubleclick instead")]
|
||||||
ondoubleclick
|
|
||||||
|
|
||||||
/// ondoubleclick
|
|
||||||
ondblclick
|
ondblclick
|
||||||
|
|
||||||
/// onmousedown
|
/// onmousedown
|
||||||
@@ -149,6 +146,22 @@ impl_event! {
|
|||||||
onmouseup
|
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 {
|
impl MouseData {
|
||||||
/// Construct MouseData with the specified properties
|
/// 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 {
|
macro_rules! trait_methods {
|
||||||
(
|
(
|
||||||
@base
|
@base
|
||||||
$(#[$trait_attr:meta])*
|
$(#[$trait_attr:meta])*
|
||||||
$trait:ident;
|
$trait:ident;
|
||||||
$fn:ident;
|
$fn:ident;
|
||||||
|
$fn_html_to_rsx:ident;
|
||||||
$(
|
$(
|
||||||
$(#[$attr:meta])*
|
$(#[$attr:meta])*
|
||||||
$name:ident $(: $($arg:literal),*)*;
|
$name:ident $(: $($arg:literal),*)*;
|
||||||
@@ -62,6 +94,18 @@ macro_rules! trait_methods {
|
|||||||
)*
|
)*
|
||||||
None
|
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
|
// Rename the incoming ident and apply a custom namespace
|
||||||
@@ -79,6 +123,7 @@ trait_methods! {
|
|||||||
|
|
||||||
GlobalAttributes;
|
GlobalAttributes;
|
||||||
map_global_attributes;
|
map_global_attributes;
|
||||||
|
map_html_global_attributes_to_rsx;
|
||||||
|
|
||||||
/// Prevent the default action for this element.
|
/// Prevent the default action for this element.
|
||||||
///
|
///
|
||||||
@@ -1593,6 +1638,7 @@ trait_methods! {
|
|||||||
@base
|
@base
|
||||||
SvgAttributes;
|
SvgAttributes;
|
||||||
map_svg_attributes;
|
map_svg_attributes;
|
||||||
|
map_html_svg_attributes_to_rsx;
|
||||||
|
|
||||||
/// Prevent the default action for this element.
|
/// Prevent the default action for this element.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
mod elements;
|
mod elements;
|
||||||
#[cfg(feature = "hot-reload-context")]
|
#[cfg(feature = "hot-reload-context")]
|
||||||
pub use elements::HtmlCtx;
|
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 events;
|
||||||
pub mod geometry;
|
pub mod geometry;
|
||||||
mod global_attributes;
|
mod global_attributes;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ warp = { version = "0.3.3", optional = true }
|
|||||||
axum = { version = "0.6.1", optional = true, features = ["ws"] }
|
axum = { version = "0.6.1", optional = true, features = ["ws"] }
|
||||||
|
|
||||||
# salvo
|
# salvo
|
||||||
salvo = { version = "0.44.1", optional = true, features = ["ws"] }
|
salvo = { version = "0.63.0", optional = true, features = ["websocket"] }
|
||||||
once_cell = "1.17.1"
|
once_cell = "1.17.1"
|
||||||
async-trait = "0.1.71"
|
async-trait = "0.1.71"
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ tokio = { workspace = true, features = ["full"] }
|
|||||||
dioxus = { workspace = true }
|
dioxus = { workspace = true }
|
||||||
warp = "0.3.3"
|
warp = "0.3.3"
|
||||||
axum = { version = "0.6.1", features = ["ws"] }
|
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 = "0.5.0"
|
||||||
rocket_ws = "0.1.0"
|
rocket_ws = "0.1.0"
|
||||||
tower = "0.4.13"
|
tower = "0.4.13"
|
||||||
|
|||||||
@@ -92,10 +92,10 @@ impl State for TaffyLayout {
|
|||||||
attribute, value, ..
|
attribute, value, ..
|
||||||
} in attributes
|
} in attributes
|
||||||
{
|
{
|
||||||
if let Some(text) = value.as_text() {
|
if value.as_custom().is_none() {
|
||||||
apply_layout_attributes_cfg(
|
apply_layout_attributes_cfg(
|
||||||
&attribute.name,
|
&attribute.name,
|
||||||
text,
|
&value.to_string(),
|
||||||
&mut style,
|
&mut style,
|
||||||
&LayoutConfigeration {
|
&LayoutConfigeration {
|
||||||
border_widths: BorderWidths {
|
border_widths: BorderWidths {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ keywords = ["dom", "ui", "gui", "react"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus-autofmt = { workspace = true }
|
dioxus-autofmt = { workspace = true }
|
||||||
dioxus-rsx = { workspace = true }
|
dioxus-rsx = { workspace = true }
|
||||||
|
dioxus-html = { workspace = true, features = ["html-to-rsx"]}
|
||||||
html_parser.workspace = true
|
html_parser.workspace = true
|
||||||
proc-macro2 = "1.0.49"
|
proc-macro2 = "1.0.49"
|
||||||
quote = "1.0.23"
|
quote = "1.0.23"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
|
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||||
|
|
||||||
use convert_case::{Case, Casing};
|
use convert_case::{Case, Casing};
|
||||||
|
use dioxus_html::{map_html_attribute_to_rsx, map_html_element_to_rsx};
|
||||||
use dioxus_rsx::{
|
use dioxus_rsx::{
|
||||||
BodyNode, CallBody, Component, Element, ElementAttr, ElementAttrNamed, ElementName, IfmtInput,
|
BodyNode, CallBody, Component, Element, ElementAttr, ElementAttrNamed, ElementName, IfmtInput,
|
||||||
};
|
};
|
||||||
@@ -24,26 +25,41 @@ pub fn rsx_node_from_html(node: &Node) -> Option<BodyNode> {
|
|||||||
match node {
|
match node {
|
||||||
Node::Text(text) => Some(BodyNode::Text(ifmt_from_text(text))),
|
Node::Text(text) => Some(BodyNode::Text(ifmt_from_text(text))),
|
||||||
Node::Element(el) => {
|
Node::Element(el) => {
|
||||||
let el_name = el.name.to_case(Case::Snake);
|
let el_name = if let Some(name) = map_html_element_to_rsx(&el.name) {
|
||||||
let el_name = ElementName::Ident(Ident::new(el_name.as_str(), Span::call_site()));
|
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
|
let mut attributes: Vec<_> = el
|
||||||
.attributes
|
.attributes
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(name, value)| {
|
.map(|(name, value)| {
|
||||||
let ident = if matches!(name.as_str(), "for" | "async" | "type" | "as") {
|
let value = ifmt_from_text(value.as_deref().unwrap_or("false"));
|
||||||
Ident::new_raw(name.as_str(), Span::call_site())
|
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 {
|
} else {
|
||||||
let new_name = name.to_case(Case::Snake);
|
// If we don't recognize the attribute, we assume it's a custom attribute
|
||||||
Ident::new(new_name.as_str(), Span::call_site())
|
ElementAttr::CustomAttrText {
|
||||||
|
value,
|
||||||
|
name: LitStr::new(name, Span::call_site()),
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ElementAttrNamed {
|
ElementAttrNamed {
|
||||||
el_name: el_name.clone(),
|
el_name: el_name.clone(),
|
||||||
attr: ElementAttr::AttrText {
|
attr,
|
||||||
value: ifmt_from_text(value.as_deref().unwrap_or("false")),
|
|
||||||
name: ident,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.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 id="asd">hello world!</div>
|
||||||
<div for="asd">hello world!</div>
|
<div for="asd">hello world!</div>
|
||||||
<div async="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>
|
</div>
|
||||||
"#
|
"#
|
||||||
.trim();
|
.trim();
|
||||||
@@ -28,8 +26,6 @@ fn simple_elements() {
|
|||||||
div { id: "asd", "hello world!" }
|
div { id: "asd", "hello world!" }
|
||||||
div { r#for: "asd", "hello world!" }
|
div { r#for: "asd", "hello world!" }
|
||||||
div { r#async: "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);
|
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);
|
||||||
|
}
|
||||||
+11
-6
@@ -204,12 +204,17 @@ impl<'a> ToTokens for TemplateRenderer<'a> {
|
|||||||
None => quote! { None },
|
None => quote! { None },
|
||||||
};
|
};
|
||||||
|
|
||||||
let spndbg = format!("{:?}", self.roots[0].span());
|
let root_col = match self.roots.first() {
|
||||||
let root_col = spndbg
|
Some(first_root) => {
|
||||||
.rsplit_once("..")
|
let first_root_span = format!("{:?}", first_root.span());
|
||||||
.and_then(|(_, after)| after.split_once(')').map(|(before, _)| before))
|
first_root_span
|
||||||
.unwrap_or_default();
|
.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)| {
|
let root_printer = self.roots.iter().enumerate().map(|(idx, root)| {
|
||||||
context.current_path.push(idx as u8);
|
context.current_path.push(idx as u8);
|
||||||
let out = context.render_static_node(root);
|
let out = context.render_static_node(root);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use syn::{
|
|||||||
/// are enabled), it will instead make a network request to the server.
|
/// are enabled), it will instead make a network request to the server.
|
||||||
///
|
///
|
||||||
/// You can specify one, two, or three arguments to the server function:
|
/// 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`).
|
/// (e.g., `MyServerFn`).
|
||||||
/// 2. *Optional*: A URL prefix at which the function will be mounted when it’s registered
|
/// 2. *Optional*: A URL prefix at which the function will be mounted when it’s registered
|
||||||
/// (e.g., `"/api"`). Defaults to `"/"`.
|
/// (e.g., `"/api"`). Defaults to `"/"`.
|
||||||
|
|||||||
@@ -495,3 +495,9 @@ impl<T> Deref for ReadOnlySignal<T> {
|
|||||||
reference_to_closure as &Self::Target
|
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,
|
memory_cache_limit: usize,
|
||||||
invalidate_after: Option<Duration>,
|
invalidate_after: Option<Duration>,
|
||||||
map_path: Option<PathMapFn>,
|
map_path: Option<PathMapFn>,
|
||||||
|
clear_cache: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for IncrementalRendererConfig {
|
impl Default for IncrementalRendererConfig {
|
||||||
@@ -83,9 +84,16 @@ impl IncrementalRendererConfig {
|
|||||||
memory_cache_limit: 10000,
|
memory_cache_limit: 10000,
|
||||||
invalidate_after: None,
|
invalidate_after: None,
|
||||||
map_path: 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`.
|
/// 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.
|
/// 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 {
|
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.
|
/// Build the incremental renderer.
|
||||||
pub fn build(self) -> IncrementalRenderer {
|
pub fn build(self) -> IncrementalRenderer {
|
||||||
let static_dir = self.static_dir.clone();
|
let static_dir = self.static_dir.clone();
|
||||||
IncrementalRenderer {
|
let mut renderer = IncrementalRenderer {
|
||||||
static_dir: self.static_dir.clone(),
|
static_dir: self.static_dir.clone(),
|
||||||
memory_cache: NonZeroUsize::new(self.memory_cache_limit)
|
memory_cache: NonZeroUsize::new(self.memory_cache_limit)
|
||||||
.map(|limit| lru::LruCache::with_hasher(limit, Default::default())),
|
.map(|limit| lru::LruCache::with_hasher(limit, Default::default())),
|
||||||
@@ -129,6 +137,12 @@ impl IncrementalRendererConfig {
|
|||||||
path
|
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>");
|
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] = &[
|
pub(crate) const BOOL_ATTRS: &[&str] = &[
|
||||||
"allowfullscreen",
|
"allowfullscreen",
|
||||||
"allowpaymentrequest",
|
"allowpaymentrequest",
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ pub static BUILTIN_INTERNED_STRINGS: &[&str] = &[
|
|||||||
"oncopy",
|
"oncopy",
|
||||||
"oncuechange",
|
"oncuechange",
|
||||||
"oncut",
|
"oncut",
|
||||||
"ondblclick",
|
"ondoubleclick",
|
||||||
"ondrag",
|
"ondrag",
|
||||||
"ondragend",
|
"ondragend",
|
||||||
"ondragenter",
|
"ondragenter",
|
||||||
|
|||||||
Reference in new issue
Block a user