mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-26 14:10:20 +00:00
Merge branch 'master' into master
This commit is contained in:
commit
601627d46e
128 changed files with 2793 additions and 790 deletions
7
.github/workflows/main.yml
vendored
7
.github/workflows/main.yml
vendored
|
@ -124,8 +124,6 @@ jobs:
|
|||
}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: install stable
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
|
@ -141,6 +139,11 @@ jobs:
|
|||
workspaces: core -> ../target
|
||||
save-if: ${{ matrix.features.key == 'all' }}
|
||||
|
||||
- name: Install rustfmt
|
||||
run: rustup component add rustfmt
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: test
|
||||
run: |
|
||||
${{ env.RUST_CARGO_COMMAND }} ${{ matrix.platform.command }} ${{ matrix.platform.args }} --target ${{ matrix.platform.target }}
|
||||
|
|
3
.github/workflows/miri.yml
vendored
3
.github/workflows/miri.yml
vendored
|
@ -86,8 +86,7 @@ jobs:
|
|||
|
||||
# working-directory: tokio
|
||||
env:
|
||||
# todo: disable memory leaks ignore
|
||||
MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance -Zmiri-retag-fields -Zmiri-ignore-leaks
|
||||
MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance -Zmiri-retag-fields
|
||||
PROPTEST_CASES: 10
|
||||
|
||||
# Cache the global cargo directory, but NOT the local `target` directory which
|
||||
|
|
2
.github/workflows/playwright.yml
vendored
2
.github/workflows/playwright.yml
vendored
|
@ -43,7 +43,7 @@ jobs:
|
|||
# args: --path packages/cli
|
||||
- name: Run Playwright tests
|
||||
run: npx playwright test
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -4,6 +4,7 @@
|
|||
/dist
|
||||
Cargo.lock
|
||||
.DS_Store
|
||||
/examples/assets/test_video.mp4
|
||||
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
|
|
|
@ -50,7 +50,7 @@ members = [
|
|||
exclude = ["examples/mobile_demo"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
|
||||
# dependencies that are shared across packages
|
||||
[workspace.dependencies]
|
||||
|
@ -77,7 +77,7 @@ dioxus-native-core = { path = "packages/native-core", version = "0.4.0" }
|
|||
dioxus-native-core-macro = { path = "packages/native-core-macro", version = "0.4.0" }
|
||||
rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.4.0" }
|
||||
dioxus-signals = { path = "packages/signals" }
|
||||
generational-box = { path = "packages/generational-box", version = "0.1.0" }
|
||||
generational-box = { path = "packages/generational-box", version = "0.4.3" }
|
||||
dioxus-hot-reload = { path = "packages/hot-reload", version = "0.4.0" }
|
||||
dioxus-fullstack = { path = "packages/fullstack", version = "0.4.1" }
|
||||
dioxus_server_macro = { path = "packages/server-macro", version = "0.4.1" }
|
||||
|
@ -99,7 +99,7 @@ prettyplease = { package = "prettier-please", version = "0.2", features = [
|
|||
# It is not meant to be published, but is used so "cargo run --example XYZ" works properly
|
||||
[package]
|
||||
name = "dioxus-examples"
|
||||
version = "0.0.0"
|
||||
version = "0.4.3"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2021"
|
||||
description = "Top level crate for the Dioxus repository"
|
||||
|
@ -133,3 +133,4 @@ fern = { version = "0.6.0", features = ["colored"] }
|
|||
env_logger = "0.10.0"
|
||||
simple_logger = "4.0.0"
|
||||
thiserror = { workspace = true }
|
||||
http-range = "0.1.5"
|
||||
|
|
|
@ -161,7 +161,7 @@ So... Dioxus is great, but why won't it work for me?
|
|||
## Contributing
|
||||
- Check out the website [section on contributing](https://dioxuslabs.com/learn/0.4/contributing).
|
||||
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
||||
- Join the discord and ask questions!
|
||||
- [Join](https://discord.gg/XgGxMSkvUM) the discord and ask questions!
|
||||
|
||||
|
||||
<a href="https://github.com/dioxuslabs/dioxus/graphs/contributors">
|
||||
|
|
|
@ -53,8 +53,7 @@ fn app(cx: Scope) -> Element {
|
|||
};
|
||||
|
||||
cx.render(rsx! (
|
||||
div {
|
||||
style: "{CONTAINER_STYLE}",
|
||||
div { style: "{CONTAINER_STYLE}",
|
||||
div {
|
||||
style: "{RECT_STYLE}",
|
||||
// focusing is necessary to catch keyboard events
|
||||
|
@ -62,7 +61,7 @@ fn app(cx: Scope) -> Element {
|
|||
|
||||
onmousemove: move |event| log_event(Event::MouseMove(event)),
|
||||
onclick: move |event| log_event(Event::MouseClick(event)),
|
||||
ondblclick: move |event| log_event(Event::MouseDoubleClick(event)),
|
||||
ondoubleclick: move |event| log_event(Event::MouseDoubleClick(event)),
|
||||
onmousedown: move |event| log_event(Event::MouseDown(event)),
|
||||
onmouseup: move |event| log_event(Event::MouseUp(event)),
|
||||
|
||||
|
@ -77,9 +76,7 @@ fn app(cx: Scope) -> Element {
|
|||
|
||||
"Hover, click, type or scroll to see the info down below"
|
||||
}
|
||||
div {
|
||||
events.read().iter().map(|event| rsx!( div { "{event:?}" } ))
|
||||
},
|
||||
},
|
||||
div { events.read().iter().map(|event| rsx!( div { "{event:?}" } )) }
|
||||
}
|
||||
))
|
||||
}
|
||||
|
|
29
examples/dynamic_asset.rs
Normal file
29
examples/dynamic_asset.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
use dioxus::prelude::*;
|
||||
use dioxus_desktop::wry::http::Response;
|
||||
use dioxus_desktop::{use_asset_handler, AssetRequest};
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
dioxus_desktop::launch(app);
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
use_asset_handler(cx, |request: &AssetRequest| {
|
||||
let path = request.path().to_path_buf();
|
||||
async move {
|
||||
if path != Path::new("logo.png") {
|
||||
return None;
|
||||
}
|
||||
let image_data: &[u8] = include_bytes!("./assets/logo.png");
|
||||
Some(Response::new(image_data.into()))
|
||||
}
|
||||
});
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
img {
|
||||
src: "logo.png"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
|
@ -35,7 +35,7 @@ frameworks = ["WebKit"]
|
|||
[dependencies]
|
||||
anyhow = "1.0.56"
|
||||
log = "0.4.11"
|
||||
wry = "0.28.0"
|
||||
wry = "0.34.0"
|
||||
dioxus = { path = "../../packages/dioxus" }
|
||||
dioxus-desktop = { path = "../../packages/desktop", features = [
|
||||
"tokio_runtime",
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
name = "openid_auth_demo"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
|
|
@ -16,8 +16,20 @@ fn app(cx: Scope) -> Element {
|
|||
a: "asd".to_string(),
|
||||
c: "asd".to_string(),
|
||||
d: Some("asd".to_string()),
|
||||
e: Some("asd".to_string()),
|
||||
}
|
||||
Button {
|
||||
a: "asd".to_string(),
|
||||
b: "asd".to_string(),
|
||||
c: "asd".to_string(),
|
||||
d: Some("asd".to_string()),
|
||||
e: "asd".to_string(),
|
||||
}
|
||||
Button {
|
||||
a: "asd".to_string(),
|
||||
c: "asd".to_string(),
|
||||
d: Some("asd".to_string()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
name = "query_segments_demo"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
|
|
@ -18,4 +18,4 @@ dioxus = { path = "../../packages/dioxus" }
|
|||
dioxus-desktop = { path = "../../packages/desktop" }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
dioxus-web = { path = "../../packages/web" }
|
||||
dioxus-web = { path = "../../packages/web" }
|
||||
|
|
|
@ -48,11 +48,8 @@ pub fn app(cx: Scope<()>) -> Element {
|
|||
cx.render(rsx! {
|
||||
section { class: "todoapp",
|
||||
style { include_str!("./assets/todomvc.css") }
|
||||
TodoHeader {
|
||||
todos: todos,
|
||||
}
|
||||
section {
|
||||
class: "main",
|
||||
TodoHeader { todos: todos }
|
||||
section { class: "main",
|
||||
if !todos.is_empty() {
|
||||
rsx! {
|
||||
input {
|
||||
|
@ -103,31 +100,34 @@ pub fn TodoHeader<'a>(cx: Scope<'a, TodoHeaderProps<'a>>) -> Element {
|
|||
|
||||
cx.render(rsx! {
|
||||
header { class: "header",
|
||||
h1 {"todos"}
|
||||
input {
|
||||
class: "new-todo",
|
||||
placeholder: "What needs to be done?",
|
||||
value: "{draft}",
|
||||
autofocus: "true",
|
||||
oninput: move |evt| {
|
||||
draft.set(evt.value.clone());
|
||||
},
|
||||
onkeydown: move |evt| {
|
||||
if evt.key() == Key::Enter && !draft.is_empty() {
|
||||
cx.props.todos.make_mut().insert(
|
||||
**todo_id,
|
||||
TodoItem {
|
||||
id: **todo_id,
|
||||
checked: false,
|
||||
contents: draft.to_string(),
|
||||
},
|
||||
);
|
||||
*todo_id.make_mut() += 1;
|
||||
draft.set("".to_string());
|
||||
h1 { "todos" }
|
||||
input {
|
||||
class: "new-todo",
|
||||
placeholder: "What needs to be done?",
|
||||
value: "{draft}",
|
||||
autofocus: "true",
|
||||
oninput: move |evt| {
|
||||
draft.set(evt.value.clone());
|
||||
},
|
||||
onkeydown: move |evt| {
|
||||
if evt.key() == Key::Enter && !draft.is_empty() {
|
||||
cx.props
|
||||
.todos
|
||||
.make_mut()
|
||||
.insert(
|
||||
**todo_id,
|
||||
TodoItem {
|
||||
id: **todo_id,
|
||||
checked: false,
|
||||
contents: draft.to_string(),
|
||||
},
|
||||
);
|
||||
*todo_id.make_mut() += 1;
|
||||
draft.set("".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -146,8 +146,7 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
|
|||
let editing = if **is_editing { "editing" } else { "" };
|
||||
|
||||
cx.render(rsx!{
|
||||
li {
|
||||
class: "{completed} {editing}",
|
||||
li { class: "{completed} {editing}",
|
||||
div { class: "view",
|
||||
input {
|
||||
class: "toggle",
|
||||
|
@ -160,14 +159,16 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
|
|||
}
|
||||
label {
|
||||
r#for: "cbg-{todo.id}",
|
||||
ondblclick: move |_| is_editing.set(true),
|
||||
ondoubleclick: move |_| is_editing.set(true),
|
||||
prevent_default: "onclick",
|
||||
"{todo.contents}"
|
||||
}
|
||||
button {
|
||||
class: "destroy",
|
||||
onclick: move |_| { cx.props.todos.make_mut().remove(&todo.id); },
|
||||
prevent_default: "onclick",
|
||||
onclick: move |_| {
|
||||
cx.props.todos.make_mut().remove(&todo.id);
|
||||
},
|
||||
prevent_default: "onclick"
|
||||
}
|
||||
}
|
||||
is_editing.then(|| rsx!{
|
||||
|
@ -213,15 +214,15 @@ pub fn ListFooter<'a>(cx: Scope<'a, ListFooterProps<'a>>) -> Element {
|
|||
cx.render(rsx! {
|
||||
footer { class: "footer",
|
||||
span { class: "todo-count",
|
||||
strong {"{active_todo_count} "}
|
||||
span {"{active_todo_text} left"}
|
||||
strong { "{active_todo_count} " }
|
||||
span { "{active_todo_text} left" }
|
||||
}
|
||||
ul { class: "filters",
|
||||
for (state, state_text, url) in [
|
||||
(FilterState::All, "All", "#/"),
|
||||
(FilterState::Active, "Active", "#/active"),
|
||||
(FilterState::Completed, "Completed", "#/completed"),
|
||||
] {
|
||||
for (state , state_text , url) in [
|
||||
(FilterState::All, "All", "#/"),
|
||||
(FilterState::Active, "Active", "#/active"),
|
||||
(FilterState::Completed, "Completed", "#/completed"),
|
||||
] {
|
||||
li {
|
||||
a {
|
||||
href: url,
|
||||
|
@ -250,8 +251,14 @@ pub fn PageFooter(cx: Scope) -> Element {
|
|||
cx.render(rsx! {
|
||||
footer { class: "info",
|
||||
p { "Double-click to edit a todo" }
|
||||
p { "Created by ", a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }}
|
||||
p { "Part of ", a { href: "http://todomvc.com", "TodoMVC" }}
|
||||
p {
|
||||
"Created by "
|
||||
a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }
|
||||
}
|
||||
p {
|
||||
"Part of "
|
||||
a { href: "http://todomvc.com", "TodoMVC" }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
184
examples/video_stream.rs
Normal file
184
examples/video_stream.rs
Normal file
|
@ -0,0 +1,184 @@
|
|||
use dioxus::prelude::*;
|
||||
use dioxus_desktop::wry::http;
|
||||
use dioxus_desktop::wry::http::Response;
|
||||
use dioxus_desktop::{use_asset_handler, AssetRequest};
|
||||
use http::{header::*, response::Builder as ResponseBuilder, status::StatusCode};
|
||||
use std::borrow::Cow;
|
||||
use std::{io::SeekFrom, path::PathBuf};
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncSeekExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
const VIDEO_PATH: &str = "./examples/assets/test_video.mp4";
|
||||
|
||||
fn main() {
|
||||
let video_file = PathBuf::from(VIDEO_PATH);
|
||||
if !video_file.exists() {
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(async move {
|
||||
println!("Downloading video file...");
|
||||
let video_url =
|
||||
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
|
||||
let mut response = reqwest::get(video_url).await.unwrap();
|
||||
let mut file = tokio::fs::File::create(&video_file).await.unwrap();
|
||||
while let Some(chunk) = response.chunk().await.unwrap() {
|
||||
file.write_all(&chunk).await.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
dioxus_desktop::launch(app);
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
use_asset_handler(cx, move |request: &AssetRequest| {
|
||||
let request = request.clone();
|
||||
async move {
|
||||
let video_file = PathBuf::from(VIDEO_PATH);
|
||||
let mut file = tokio::fs::File::open(&video_file).await.unwrap();
|
||||
let response: Option<Response<Cow<'static, [u8]>>> =
|
||||
match get_stream_response(&mut file, &request).await {
|
||||
Ok(response) => Some(response.map(Cow::Owned)),
|
||||
Err(err) => {
|
||||
eprintln!("Error: {}", err);
|
||||
None
|
||||
}
|
||||
};
|
||||
response
|
||||
}
|
||||
});
|
||||
|
||||
render! {
|
||||
div { video { src: "test_video.mp4", autoplay: true, controls: true, width: 640, height: 480 } }
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_stream_response(
|
||||
asset: &mut (impl tokio::io::AsyncSeek + tokio::io::AsyncRead + Unpin + Send + Sync),
|
||||
request: &AssetRequest,
|
||||
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
|
||||
// get stream length
|
||||
let len = {
|
||||
let old_pos = asset.stream_position().await?;
|
||||
let len = asset.seek(SeekFrom::End(0)).await?;
|
||||
asset.seek(SeekFrom::Start(old_pos)).await?;
|
||||
len
|
||||
};
|
||||
|
||||
let mut resp = ResponseBuilder::new().header(CONTENT_TYPE, "video/mp4");
|
||||
|
||||
// if the webview sent a range header, we need to send a 206 in return
|
||||
// Actually only macOS and Windows are supported. Linux will ALWAYS return empty headers.
|
||||
let http_response = if let Some(range_header) = request.headers().get("range") {
|
||||
let not_satisfiable = || {
|
||||
ResponseBuilder::new()
|
||||
.status(StatusCode::RANGE_NOT_SATISFIABLE)
|
||||
.header(CONTENT_RANGE, format!("bytes */{len}"))
|
||||
.body(vec![])
|
||||
};
|
||||
|
||||
// parse range header
|
||||
let ranges = if let Ok(ranges) = http_range::HttpRange::parse(range_header.to_str()?, len) {
|
||||
ranges
|
||||
.iter()
|
||||
// map the output back to spec range <start-end>, example: 0-499
|
||||
.map(|r| (r.start, r.start + r.length - 1))
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
return Ok(not_satisfiable()?);
|
||||
};
|
||||
|
||||
/// The Maximum bytes we send in one range
|
||||
const MAX_LEN: u64 = 1000 * 1024;
|
||||
|
||||
if ranges.len() == 1 {
|
||||
let &(start, mut end) = ranges.first().unwrap();
|
||||
|
||||
// check if a range is not satisfiable
|
||||
//
|
||||
// this should be already taken care of by HttpRange::parse
|
||||
// but checking here again for extra assurance
|
||||
if start >= len || end >= len || end < start {
|
||||
return Ok(not_satisfiable()?);
|
||||
}
|
||||
|
||||
// adjust end byte for MAX_LEN
|
||||
end = start + (end - start).min(len - start).min(MAX_LEN - 1);
|
||||
|
||||
// calculate number of bytes needed to be read
|
||||
let bytes_to_read = end + 1 - start;
|
||||
|
||||
// allocate a buf with a suitable capacity
|
||||
let mut buf = Vec::with_capacity(bytes_to_read as usize);
|
||||
// seek the file to the starting byte
|
||||
asset.seek(SeekFrom::Start(start)).await?;
|
||||
// read the needed bytes
|
||||
asset.take(bytes_to_read).read_to_end(&mut buf).await?;
|
||||
|
||||
resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}"));
|
||||
resp = resp.header(CONTENT_LENGTH, end + 1 - start);
|
||||
resp = resp.status(StatusCode::PARTIAL_CONTENT);
|
||||
resp.body(buf)
|
||||
} else {
|
||||
let mut buf = Vec::new();
|
||||
let ranges = ranges
|
||||
.iter()
|
||||
.filter_map(|&(start, mut end)| {
|
||||
// filter out unsatisfiable ranges
|
||||
//
|
||||
// this should be already taken care of by HttpRange::parse
|
||||
// but checking here again for extra assurance
|
||||
if start >= len || end >= len || end < start {
|
||||
None
|
||||
} else {
|
||||
// adjust end byte for MAX_LEN
|
||||
end = start + (end - start).min(len - start).min(MAX_LEN - 1);
|
||||
Some((start, end))
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let boundary = format!("{:x}", rand::random::<u64>());
|
||||
let boundary_sep = format!("\r\n--{boundary}\r\n");
|
||||
let boundary_closer = format!("\r\n--{boundary}\r\n");
|
||||
|
||||
resp = resp.header(
|
||||
CONTENT_TYPE,
|
||||
format!("multipart/byteranges; boundary={boundary}"),
|
||||
);
|
||||
|
||||
for (end, start) in ranges {
|
||||
// a new range is being written, write the range boundary
|
||||
buf.write_all(boundary_sep.as_bytes()).await?;
|
||||
|
||||
// write the needed headers `Content-Type` and `Content-Range`
|
||||
buf.write_all(format!("{CONTENT_TYPE}: video/mp4\r\n").as_bytes())
|
||||
.await?;
|
||||
buf.write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes())
|
||||
.await?;
|
||||
|
||||
// write the separator to indicate the start of the range body
|
||||
buf.write_all("\r\n".as_bytes()).await?;
|
||||
|
||||
// calculate number of bytes needed to be read
|
||||
let bytes_to_read = end + 1 - start;
|
||||
|
||||
let mut local_buf = vec![0_u8; bytes_to_read as usize];
|
||||
asset.seek(SeekFrom::Start(start)).await?;
|
||||
asset.read_exact(&mut local_buf).await?;
|
||||
buf.extend_from_slice(&local_buf);
|
||||
}
|
||||
// all ranges have been written, write the closing boundary
|
||||
buf.write_all(boundary_closer.as_bytes()).await?;
|
||||
|
||||
resp.body(buf)
|
||||
}
|
||||
} else {
|
||||
resp = resp.header(CONTENT_LENGTH, len);
|
||||
let mut buf = Vec::with_capacity(len as usize);
|
||||
asset.read_to_end(&mut buf).await?;
|
||||
resp.body(buf)
|
||||
};
|
||||
|
||||
http_response.map_err(Into::into)
|
||||
}
|
|
@ -8,13 +8,14 @@ use std::fmt::{Result, Write};
|
|||
|
||||
use dioxus_rsx::IfmtInput;
|
||||
|
||||
use crate::write_ifmt;
|
||||
use crate::{indent::IndentOptions, write_ifmt};
|
||||
|
||||
/// The output buffer that tracks indent and string
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Buffer {
|
||||
pub buf: String,
|
||||
pub indent: usize,
|
||||
pub indent_level: usize,
|
||||
pub indent: IndentOptions,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
|
@ -31,16 +32,16 @@ impl Buffer {
|
|||
}
|
||||
|
||||
pub fn tab(&mut self) -> Result {
|
||||
self.write_tabs(self.indent)
|
||||
self.write_tabs(self.indent_level)
|
||||
}
|
||||
|
||||
pub fn indented_tab(&mut self) -> Result {
|
||||
self.write_tabs(self.indent + 1)
|
||||
self.write_tabs(self.indent_level + 1)
|
||||
}
|
||||
|
||||
pub fn write_tabs(&mut self, num: usize) -> std::fmt::Result {
|
||||
for _ in 0..num {
|
||||
write!(self.buf, " ")?
|
||||
write!(self.buf, "{}", self.indent.indent_str())?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ impl Writer<'_> {
|
|||
|
||||
// check if we have a lot of attributes
|
||||
let attr_len = self.is_short_attrs(attributes);
|
||||
let is_short_attr_list = (attr_len + self.out.indent * 4) < 80;
|
||||
let is_short_attr_list = (attr_len + self.out.indent_level * 4) < 80;
|
||||
let children_len = self.is_short_children(children);
|
||||
let is_small_children = children_len.is_some();
|
||||
|
||||
|
@ -86,7 +86,7 @@ impl Writer<'_> {
|
|||
|
||||
// if we have few children and few attributes, make it a one-liner
|
||||
if is_short_attr_list && is_small_children {
|
||||
if children_len.unwrap() + attr_len + self.out.indent * 4 < 100 {
|
||||
if children_len.unwrap() + attr_len + self.out.indent_level * 4 < 100 {
|
||||
opt_level = ShortOptimization::Oneliner;
|
||||
} else {
|
||||
opt_level = ShortOptimization::PropsOnTop;
|
||||
|
@ -185,11 +185,11 @@ impl Writer<'_> {
|
|||
}
|
||||
|
||||
while let Some(attr) = attr_iter.next() {
|
||||
self.out.indent += 1;
|
||||
self.out.indent_level += 1;
|
||||
if !sameline {
|
||||
self.write_comments(attr.attr.start())?;
|
||||
}
|
||||
self.out.indent -= 1;
|
||||
self.out.indent_level -= 1;
|
||||
|
||||
if !sameline {
|
||||
self.out.indented_tabbed_line()?;
|
||||
|
@ -398,14 +398,14 @@ impl Writer<'_> {
|
|||
for idx in start.line..end.line {
|
||||
let line = &self.src[idx];
|
||||
if line.trim().starts_with("//") {
|
||||
for _ in 0..self.out.indent + 1 {
|
||||
for _ in 0..self.out.indent_level + 1 {
|
||||
write!(self.out, " ")?
|
||||
}
|
||||
writeln!(self.out, "{}", line.trim()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
for _ in 0..self.out.indent {
|
||||
for _ in 0..self.out.indent_level {
|
||||
write!(self.out, " ")?
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ impl Writer<'_> {
|
|||
let first_line = &self.src[start.line - 1];
|
||||
write!(self.out, "{}", &first_line[start.column - 1..].trim_start())?;
|
||||
|
||||
let prev_block_indent_level = crate::leading_whitespaces(first_line) / 4;
|
||||
let prev_block_indent_level = self.out.indent.count_indents(first_line);
|
||||
|
||||
for (id, line) in self.src[start.line..end.line].iter().enumerate() {
|
||||
writeln!(self.out)?;
|
||||
|
@ -43,9 +43,9 @@ impl Writer<'_> {
|
|||
};
|
||||
|
||||
// trim the leading whitespace
|
||||
let previous_indent = crate::leading_whitespaces(line) / 4;
|
||||
let previous_indent = self.out.indent.count_indents(line);
|
||||
let offset = previous_indent.saturating_sub(prev_block_indent_level);
|
||||
let required_indent = self.out.indent + offset;
|
||||
let required_indent = self.out.indent_level + offset;
|
||||
self.out.write_tabs(required_indent)?;
|
||||
|
||||
let line = line.trim_start();
|
||||
|
|
108
packages/autofmt/src/indent.rs
Normal file
108
packages/autofmt/src/indent.rs
Normal file
|
@ -0,0 +1,108 @@
|
|||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum IndentType {
|
||||
Spaces,
|
||||
Tabs,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IndentOptions {
|
||||
width: usize,
|
||||
indent_string: String,
|
||||
}
|
||||
|
||||
impl IndentOptions {
|
||||
pub fn new(typ: IndentType, width: usize) -> Self {
|
||||
assert_ne!(width, 0, "Cannot have an indent width of 0");
|
||||
Self {
|
||||
width,
|
||||
indent_string: match typ {
|
||||
IndentType::Tabs => "\t".into(),
|
||||
IndentType::Spaces => " ".repeat(width),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a string containing one indent worth of whitespace
|
||||
pub fn indent_str(&self) -> &str {
|
||||
&self.indent_string
|
||||
}
|
||||
|
||||
/// Computes the line length in characters, counting tabs as the indent width.
|
||||
pub fn line_length(&self, line: &str) -> usize {
|
||||
line.chars()
|
||||
.map(|ch| if ch == '\t' { self.width } else { 1 })
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Estimates how many times the line has been indented.
|
||||
pub fn count_indents(&self, mut line: &str) -> usize {
|
||||
let mut indent = 0;
|
||||
while !line.is_empty() {
|
||||
// Try to count tabs
|
||||
let num_tabs = line.chars().take_while(|ch| *ch == '\t').count();
|
||||
if num_tabs > 0 {
|
||||
indent += num_tabs;
|
||||
line = &line[num_tabs..];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to count spaces
|
||||
let num_spaces = line.chars().take_while(|ch| *ch == ' ').count();
|
||||
if num_spaces >= self.width {
|
||||
// Intentionally floor here to take only the amount of space that matches an indent
|
||||
let num_space_indents = num_spaces / self.width;
|
||||
indent += num_space_indents;
|
||||
line = &line[num_space_indents * self.width..];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Line starts with either non-indent characters or an unevent amount of spaces,
|
||||
// so no more indent remains.
|
||||
break;
|
||||
}
|
||||
indent
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for IndentOptions {
|
||||
fn default() -> Self {
|
||||
Self::new(IndentType::Spaces, 4)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn count_indents() {
|
||||
assert_eq!(
|
||||
IndentOptions::new(IndentType::Spaces, 4).count_indents("no indentation here!"),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
IndentOptions::new(IndentType::Spaces, 4).count_indents("\t\tv += 2"),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
IndentOptions::new(IndentType::Spaces, 4).count_indents("\t\t v += 2"),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
IndentOptions::new(IndentType::Spaces, 2).count_indents(" v += 2"),
|
||||
2
|
||||
);
|
||||
}
|
||||
}
|
|
@ -16,8 +16,11 @@ mod collect_macros;
|
|||
mod component;
|
||||
mod element;
|
||||
mod expr;
|
||||
mod indent;
|
||||
mod writer;
|
||||
|
||||
pub use indent::{IndentOptions, IndentType};
|
||||
|
||||
/// A modification to the original file to be applied by an IDE
|
||||
///
|
||||
/// Right now this re-writes entire rsx! blocks at a time, instead of precise line-by-line changes.
|
||||
|
@ -47,7 +50,7 @@ pub struct FormattedBlock {
|
|||
/// back to the file precisely.
|
||||
///
|
||||
/// Nested blocks of RSX will be handled automatically
|
||||
pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
|
||||
pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
|
||||
let mut formatted_blocks = Vec::new();
|
||||
|
||||
let parsed = syn::parse_file(contents).unwrap();
|
||||
|
@ -61,6 +64,7 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
|
|||
}
|
||||
|
||||
let mut writer = Writer::new(contents);
|
||||
writer.out.indent = indent;
|
||||
|
||||
// Don't parse nested macros
|
||||
let mut end_span = LineColumn { column: 0, line: 0 };
|
||||
|
@ -76,7 +80,10 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
|
|||
|
||||
let rsx_start = macro_path.span().start();
|
||||
|
||||
writer.out.indent = leading_whitespaces(writer.src[rsx_start.line - 1]) / 4;
|
||||
writer.out.indent_level = writer
|
||||
.out
|
||||
.indent
|
||||
.count_indents(writer.src[rsx_start.line - 1]);
|
||||
|
||||
write_body(&mut writer, &body);
|
||||
|
||||
|
@ -159,12 +166,13 @@ pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option<String> {
|
|||
buf.consume()
|
||||
}
|
||||
|
||||
pub fn fmt_block(block: &str, indent_level: usize) -> Option<String> {
|
||||
pub fn fmt_block(block: &str, indent_level: usize, indent: IndentOptions) -> Option<String> {
|
||||
let body = syn::parse_str::<dioxus_rsx::CallBody>(block).unwrap();
|
||||
|
||||
let mut buf = Writer::new(block);
|
||||
|
||||
buf.out.indent = indent_level;
|
||||
buf.out.indent = indent;
|
||||
buf.out.indent_level = indent_level;
|
||||
|
||||
write_body(&mut buf, &body);
|
||||
|
||||
|
@ -230,14 +238,3 @@ pub(crate) fn write_ifmt(input: &IfmtInput, writable: &mut impl Write) -> std::f
|
|||
let display = DisplayIfmt(input);
|
||||
write!(writable, "{}", display)
|
||||
}
|
||||
|
||||
pub fn leading_whitespaces(input: &str) -> usize {
|
||||
input
|
||||
.chars()
|
||||
.map_while(|c| match c {
|
||||
' ' => Some(1),
|
||||
'\t' => Some(4),
|
||||
_ => None,
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
|
|
@ -96,11 +96,11 @@ impl<'a> Writer<'a> {
|
|||
|
||||
// Push out the indent level and write each component, line by line
|
||||
pub fn write_body_indented(&mut self, children: &[BodyNode]) -> Result {
|
||||
self.out.indent += 1;
|
||||
self.out.indent_level += 1;
|
||||
|
||||
self.write_body_no_indent(children)?;
|
||||
|
||||
self.out.indent -= 1;
|
||||
self.out.indent_level -= 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ macro_rules! twoway {
|
|||
#[test]
|
||||
fn $name() {
|
||||
let src = include_str!(concat!("./samples/", stringify!($name), ".rsx"));
|
||||
let formatted = dioxus_autofmt::fmt_file(src);
|
||||
let formatted = dioxus_autofmt::fmt_file(src, Default::default());
|
||||
let out = dioxus_autofmt::apply_formats(src, formatted);
|
||||
// normalize line endings
|
||||
let out = out.replace("\r", "");
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
use dioxus_autofmt::{IndentOptions, IndentType};
|
||||
|
||||
macro_rules! twoway {
|
||||
($val:literal => $name:ident) => {
|
||||
($val:literal => $name:ident ($indent:expr)) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let src_right = include_str!(concat!("./wrong/", $val, ".rsx"));
|
||||
let src_wrong = include_str!(concat!("./wrong/", $val, ".wrong.rsx"));
|
||||
let formatted = dioxus_autofmt::fmt_file(src_wrong);
|
||||
let formatted = dioxus_autofmt::fmt_file(src_wrong, $indent);
|
||||
let out = dioxus_autofmt::apply_formats(src_wrong, formatted);
|
||||
|
||||
// normalize line endings
|
||||
|
@ -16,8 +18,11 @@ macro_rules! twoway {
|
|||
};
|
||||
}
|
||||
|
||||
twoway!("comments" => comments);
|
||||
twoway!("comments-4sp" => comments_4sp (IndentOptions::new(IndentType::Spaces, 4)));
|
||||
twoway!("comments-tab" => comments_tab (IndentOptions::new(IndentType::Tabs, 4)));
|
||||
|
||||
twoway!("multi" => multi);
|
||||
twoway!("multi-4sp" => multi_4sp (IndentOptions::new(IndentType::Spaces, 4)));
|
||||
twoway!("multi-tab" => multi_tab (IndentOptions::new(IndentType::Tabs, 4)));
|
||||
|
||||
twoway!("multiexpr" => multiexpr);
|
||||
twoway!("multiexpr-4sp" => multiexpr_4sp (IndentOptions::new(IndentType::Spaces, 4)));
|
||||
twoway!("multiexpr-tab" => multiexpr_tab (IndentOptions::new(IndentType::Tabs, 4)));
|
||||
|
|
7
packages/autofmt/tests/wrong/comments-tab.rsx
Normal file
7
packages/autofmt/tests/wrong/comments-tab.rsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
rsx! {
|
||||
div {
|
||||
// Comments
|
||||
class: "asdasd",
|
||||
"hello world"
|
||||
}
|
||||
}
|
5
packages/autofmt/tests/wrong/comments-tab.wrong.rsx
Normal file
5
packages/autofmt/tests/wrong/comments-tab.wrong.rsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
rsx! {
|
||||
div {
|
||||
// Comments
|
||||
class: "asdasd", "hello world" }
|
||||
}
|
3
packages/autofmt/tests/wrong/multi-tab.rsx
Normal file
3
packages/autofmt/tests/wrong/multi-tab.rsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx! { div { "hello world" } })
|
||||
}
|
5
packages/autofmt/tests/wrong/multi-tab.wrong.rsx
Normal file
5
packages/autofmt/tests/wrong/multi-tab.wrong.rsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
div {"hello world" }
|
||||
})
|
||||
}
|
8
packages/autofmt/tests/wrong/multiexpr-tab.rsx
Normal file
8
packages/autofmt/tests/wrong/multiexpr-tab.rsx
Normal file
|
@ -0,0 +1,8 @@
|
|||
fn ItWroks() {
|
||||
cx.render(rsx! {
|
||||
div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light",
|
||||
left,
|
||||
right
|
||||
}
|
||||
})
|
||||
}
|
5
packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx
Normal file
5
packages/autofmt/tests/wrong/multiexpr-tab.wrong.rsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
fn ItWroks() {
|
||||
cx.render(rsx! {
|
||||
div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right }
|
||||
})
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "dioxus-cli"
|
||||
version = "0.4.1"
|
||||
version = "0.4.3"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2021"
|
||||
description = "CLI tool for developing, testing, and publishing Dioxus apps"
|
||||
|
@ -83,6 +83,7 @@ dioxus-html = { workspace = true, features = ["hot-reload-context"] }
|
|||
dioxus-core = { workspace = true, features = ["serialize"] }
|
||||
dioxus-hot-reload = { workspace = true }
|
||||
interprocess-docfix = { version = "1.2.2" }
|
||||
gitignore = "1.0.8"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
|
|
@ -10,7 +10,7 @@ It handles building, bundling, development and publishing to simplify developmen
|
|||
|
||||
### Install the stable version (recommended)
|
||||
|
||||
```
|
||||
```shell
|
||||
cargo install dioxus-cli
|
||||
```
|
||||
|
||||
|
@ -20,7 +20,7 @@ To get the latest bug fixes and features, you can install the development versio
|
|||
However, this is not fully tested.
|
||||
That means you're probably going to have more bugs despite having the latest bug fixes.
|
||||
|
||||
```
|
||||
```shell
|
||||
cargo install --git https://github.com/DioxusLabs/dioxus dioxus-cli
|
||||
```
|
||||
|
||||
|
@ -29,7 +29,7 @@ and install it in Cargo's global binary directory (`~/.cargo/bin/` by default).
|
|||
|
||||
### Install from local folder
|
||||
|
||||
```
|
||||
```shell
|
||||
cargo install --path . --debug
|
||||
```
|
||||
|
||||
|
@ -40,7 +40,7 @@ It will be cloned from the [dioxus-template](https://github.com/DioxusLabs/dioxu
|
|||
|
||||
Alternatively, you can specify the template path:
|
||||
|
||||
```
|
||||
```shell
|
||||
dx create hello --template gh:dioxuslabs/dioxus-template
|
||||
```
|
||||
|
||||
|
|
|
@ -48,6 +48,18 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
|
|||
|
||||
// [1] Build the .wasm module
|
||||
log::info!("🚅 Running build command...");
|
||||
|
||||
let wasm_check_command = std::process::Command::new("rustup")
|
||||
.args(["show"])
|
||||
.output()?;
|
||||
let wasm_check_output = String::from_utf8(wasm_check_command.stdout).unwrap();
|
||||
if !wasm_check_output.contains("wasm32-unknown-unknown") {
|
||||
log::info!("wasm32-unknown-unknown target not detected, installing..");
|
||||
let _ = std::process::Command::new("rustup")
|
||||
.args(["target", "add", "wasm32-unknown-unknown"])
|
||||
.output()?;
|
||||
}
|
||||
|
||||
let cmd = subprocess::Exec::cmd("cargo");
|
||||
let cmd = cmd
|
||||
.cwd(crate_dir)
|
||||
|
@ -81,6 +93,8 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
|
|||
cmd
|
||||
};
|
||||
|
||||
let cmd = cmd.args(&config.cargo_args);
|
||||
|
||||
let cmd = match executable {
|
||||
ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
|
||||
ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
|
||||
|
@ -253,7 +267,6 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
|
|||
let mut cmd = subprocess::Exec::cmd("cargo")
|
||||
.cwd(&config.crate_dir)
|
||||
.arg("build")
|
||||
.arg("--quiet")
|
||||
.arg("--message-format=json");
|
||||
|
||||
if config.release {
|
||||
|
@ -261,6 +274,8 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
|
|||
}
|
||||
if config.verbose {
|
||||
cmd = cmd.arg("--verbose");
|
||||
} else {
|
||||
cmd = cmd.arg("--quiet");
|
||||
}
|
||||
|
||||
if config.custom_profile.is_some() {
|
||||
|
@ -273,6 +288,14 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
|
|||
cmd = cmd.arg("--features").arg(features_str);
|
||||
}
|
||||
|
||||
if let Some(target) = &config.target {
|
||||
cmd = cmd.arg("--target").arg(target);
|
||||
}
|
||||
|
||||
let target_platform = config.target.as_deref().unwrap_or("");
|
||||
|
||||
cmd = cmd.args(&config.cargo_args);
|
||||
|
||||
let cmd = match &config.executable {
|
||||
crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
|
||||
crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
|
||||
|
@ -290,12 +313,17 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
|
|||
let mut res_path = match &config.executable {
|
||||
crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => {
|
||||
file_name = name.clone();
|
||||
config.target_dir.join(release_type).join(name)
|
||||
config
|
||||
.target_dir
|
||||
.join(target_platform)
|
||||
.join(release_type)
|
||||
.join(name)
|
||||
}
|
||||
crate::ExecutableType::Example(name) => {
|
||||
file_name = name.clone();
|
||||
config
|
||||
.target_dir
|
||||
.join(target_platform)
|
||||
.join(release_type)
|
||||
.join("examples")
|
||||
.join(name)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use dioxus_autofmt::{IndentOptions, IndentType};
|
||||
use futures::{stream::FuturesUnordered, StreamExt};
|
||||
use std::{fs, path::Path, process::exit};
|
||||
|
||||
|
@ -26,16 +27,19 @@ pub struct Autoformat {
|
|||
impl Autoformat {
|
||||
// Todo: autoformat the entire crate
|
||||
pub async fn autoformat(self) -> Result<()> {
|
||||
let Autoformat { check, raw, file } = self;
|
||||
|
||||
// Default to formatting the project
|
||||
if self.raw.is_none() && self.file.is_none() {
|
||||
if let Err(e) = autoformat_project(self.check).await {
|
||||
if raw.is_none() && file.is_none() {
|
||||
if let Err(e) = autoformat_project(check).await {
|
||||
eprintln!("error formatting project: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(raw) = self.raw {
|
||||
if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0) {
|
||||
if let Some(raw) = raw {
|
||||
let indent = indentation_for(".")?;
|
||||
if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0, indent) {
|
||||
println!("{}", inner);
|
||||
} else {
|
||||
// exit process with error
|
||||
|
@ -45,43 +49,90 @@ impl Autoformat {
|
|||
}
|
||||
|
||||
// Format single file
|
||||
if let Some(file) = self.file {
|
||||
let file_content = if file == "-" {
|
||||
let mut contents = String::new();
|
||||
std::io::stdin().read_to_string(&mut contents)?;
|
||||
Ok(contents)
|
||||
} else {
|
||||
fs::read_to_string(&file)
|
||||
};
|
||||
|
||||
match file_content {
|
||||
Ok(s) => {
|
||||
let edits = dioxus_autofmt::fmt_file(&s);
|
||||
let out = dioxus_autofmt::apply_formats(&s, edits);
|
||||
if file == "-" {
|
||||
print!("{}", out);
|
||||
} else {
|
||||
match fs::write(&file, out) {
|
||||
Ok(_) => {
|
||||
println!("formatted {}", file);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("failed to write formatted content to file: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("failed to open file: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
if let Some(file) = file {
|
||||
refactor_file(file)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn refactor_file(file: String) -> Result<(), Error> {
|
||||
let indent = indentation_for(".")?;
|
||||
let file_content = if file == "-" {
|
||||
let mut contents = String::new();
|
||||
std::io::stdin().read_to_string(&mut contents)?;
|
||||
Ok(contents)
|
||||
} else {
|
||||
fs::read_to_string(&file)
|
||||
};
|
||||
let Ok(s) = file_content else {
|
||||
eprintln!("failed to open file: {}", file_content.unwrap_err());
|
||||
exit(1);
|
||||
};
|
||||
let edits = dioxus_autofmt::fmt_file(&s, indent);
|
||||
let out = dioxus_autofmt::apply_formats(&s, edits);
|
||||
|
||||
if file == "-" {
|
||||
print!("{}", out);
|
||||
} else if let Err(e) = fs::write(&file, out) {
|
||||
eprintln!("failed to write formatted content to file: {e}",);
|
||||
} else {
|
||||
println!("formatted {}", file);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_project_files(config: &CrateConfig) -> Vec<PathBuf> {
|
||||
let mut files = vec![];
|
||||
|
||||
let gitignore_path = config.crate_dir.join(".gitignore");
|
||||
if gitignore_path.is_file() {
|
||||
let gitigno = gitignore::File::new(gitignore_path.as_path()).unwrap();
|
||||
if let Ok(git_files) = gitigno.included_files() {
|
||||
let git_files = git_files
|
||||
.into_iter()
|
||||
.filter(|f| f.ends_with(".rs") && !is_target_dir(f));
|
||||
files.extend(git_files)
|
||||
};
|
||||
} else {
|
||||
collect_rs_files(&config.crate_dir, &mut files);
|
||||
}
|
||||
|
||||
files
|
||||
}
|
||||
|
||||
fn is_target_dir(file: &Path) -> bool {
|
||||
let stripped = if let Ok(cwd) = std::env::current_dir() {
|
||||
file.strip_prefix(cwd).unwrap_or(file)
|
||||
} else {
|
||||
file
|
||||
};
|
||||
if let Some(first) = stripped.components().next() {
|
||||
first.as_os_str() == "target"
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
async fn format_file(
|
||||
path: impl AsRef<Path>,
|
||||
indent: IndentOptions,
|
||||
) -> Result<usize, tokio::io::Error> {
|
||||
let contents = tokio::fs::read_to_string(&path).await?;
|
||||
|
||||
let edits = dioxus_autofmt::fmt_file(&contents, indent);
|
||||
let len = edits.len();
|
||||
|
||||
if !edits.is_empty() {
|
||||
let out = dioxus_autofmt::apply_formats(&contents, edits);
|
||||
tokio::fs::write(path, out).await?;
|
||||
}
|
||||
|
||||
Ok(len)
|
||||
}
|
||||
|
||||
/// Read every .rs file accessible when considering the .gitignore and try to format it
|
||||
///
|
||||
/// Runs using Tokio for multithreading, so it should be really really fast
|
||||
|
@ -90,42 +141,27 @@ impl Autoformat {
|
|||
async fn autoformat_project(check: bool) -> Result<()> {
|
||||
let crate_config = crate::CrateConfig::new(None)?;
|
||||
|
||||
let mut files_to_format = vec![];
|
||||
collect_rs_files(&crate_config.crate_dir, &mut files_to_format);
|
||||
let files_to_format = get_project_files(&crate_config);
|
||||
|
||||
if files_to_format.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let indent = indentation_for(&files_to_format[0])?;
|
||||
|
||||
let counts = files_to_format
|
||||
.into_iter()
|
||||
.filter(|file| {
|
||||
if file.components().any(|f| f.as_os_str() == "target") {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.map(|path| async {
|
||||
let _path = path.clone();
|
||||
let res = tokio::spawn(async move {
|
||||
let contents = tokio::fs::read_to_string(&path).await?;
|
||||
|
||||
let edits = dioxus_autofmt::fmt_file(&contents);
|
||||
let len = edits.len();
|
||||
|
||||
if !edits.is_empty() {
|
||||
let out = dioxus_autofmt::apply_formats(&contents, edits);
|
||||
tokio::fs::write(&path, out).await?;
|
||||
}
|
||||
|
||||
Ok(len) as Result<usize, tokio::io::Error>
|
||||
})
|
||||
.await;
|
||||
let path_clone = path.clone();
|
||||
let res = tokio::spawn(format_file(path, indent.clone())).await;
|
||||
|
||||
match res {
|
||||
Err(err) => {
|
||||
eprintln!("error formatting file: {}\n{err}", _path.display());
|
||||
eprintln!("error formatting file: {}\n{err}", path_clone.display());
|
||||
None
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
eprintln!("error formatting file: {}\n{err}", _path.display());
|
||||
eprintln!("error formatting file: {}\n{err}", path_clone.display());
|
||||
None
|
||||
}
|
||||
Ok(Ok(res)) => Some(res),
|
||||
|
@ -135,13 +171,7 @@ async fn autoformat_project(check: bool) -> Result<()> {
|
|||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
let files_formatted: usize = counts
|
||||
.into_iter()
|
||||
.map(|f| match f {
|
||||
Some(res) => res,
|
||||
_ => 0,
|
||||
})
|
||||
.sum();
|
||||
let files_formatted: usize = counts.into_iter().flatten().sum();
|
||||
|
||||
if files_formatted > 0 && check {
|
||||
eprintln!("{} files needed formatting", files_formatted);
|
||||
|
@ -151,26 +181,67 @@ async fn autoformat_project(check: bool) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_rs_files(folder: &Path, files: &mut Vec<PathBuf>) {
|
||||
let Ok(folder) = folder.read_dir() else {
|
||||
fn indentation_for(file_or_dir: impl AsRef<Path>) -> Result<IndentOptions> {
|
||||
let out = std::process::Command::new("cargo")
|
||||
.args(["fmt", "--", "--print-config", "current"])
|
||||
.arg(file_or_dir.as_ref())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::inherit())
|
||||
.output()?;
|
||||
if !out.status.success() {
|
||||
return Err(Error::CargoError("cargo fmt failed".into()));
|
||||
}
|
||||
|
||||
let config = String::from_utf8_lossy(&out.stdout);
|
||||
|
||||
let hard_tabs = config
|
||||
.lines()
|
||||
.find(|line| line.starts_with("hard_tabs "))
|
||||
.and_then(|line| line.split_once('='))
|
||||
.map(|(_, value)| value.trim() == "true")
|
||||
.ok_or_else(|| {
|
||||
Error::RuntimeError("Could not find hard_tabs option in rustfmt config".into())
|
||||
})?;
|
||||
let tab_spaces = config
|
||||
.lines()
|
||||
.find(|line| line.starts_with("tab_spaces "))
|
||||
.and_then(|line| line.split_once('='))
|
||||
.map(|(_, value)| value.trim().parse::<usize>())
|
||||
.ok_or_else(|| {
|
||||
Error::RuntimeError("Could not find tab_spaces option in rustfmt config".into())
|
||||
})?
|
||||
.map_err(|_| {
|
||||
Error::RuntimeError("Could not parse tab_spaces option in rustfmt config".into())
|
||||
})?;
|
||||
|
||||
Ok(IndentOptions::new(
|
||||
if hard_tabs {
|
||||
IndentType::Tabs
|
||||
} else {
|
||||
IndentType::Spaces
|
||||
},
|
||||
tab_spaces,
|
||||
))
|
||||
}
|
||||
|
||||
fn collect_rs_files(folder: &impl AsRef<Path>, files: &mut Vec<PathBuf>) {
|
||||
if is_target_dir(folder.as_ref()) {
|
||||
return;
|
||||
}
|
||||
let Ok(folder) = folder.as_ref().read_dir() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// load the gitignore
|
||||
|
||||
for entry in folder {
|
||||
let Ok(entry) = entry else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
collect_rs_files(&path, files);
|
||||
}
|
||||
|
||||
if let Some(ext) = path.extension() {
|
||||
if ext == "rs" {
|
||||
if ext == "rs" && !is_target_dir(&path) {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,8 +37,14 @@ impl Build {
|
|||
.platform
|
||||
.unwrap_or(crate_config.dioxus_config.application.default_platform);
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
let _ = PluginManager::on_build_start(&crate_config, &platform);
|
||||
if let Some(target) = self.build.target {
|
||||
crate_config.set_target(target);
|
||||
}
|
||||
|
||||
crate_config.set_cargo_args(self.build.cargo_args);
|
||||
|
||||
// #[cfg(feature = "plugin")]
|
||||
// let _ = PluginManager::on_build_start(&crate_config, &platform);
|
||||
|
||||
match platform {
|
||||
Platform::Web => {
|
||||
|
@ -66,8 +72,8 @@ impl Build {
|
|||
)?;
|
||||
file.write_all(temp.as_bytes())?;
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
let _ = PluginManager::on_build_finish(&crate_config, &platform);
|
||||
// #[cfg(feature = "plugin")]
|
||||
// let _ = PluginManager::on_build_finish(&crate_config, &platform);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -76,6 +76,12 @@ impl Bundle {
|
|||
crate_config.set_profile(self.build.profile.unwrap());
|
||||
}
|
||||
|
||||
if let Some(target) = &self.build.target {
|
||||
crate_config.set_target(target.to_string());
|
||||
}
|
||||
|
||||
crate_config.set_cargo_args(self.build.cargo_args);
|
||||
|
||||
// build the desktop app
|
||||
build_desktop(&crate_config, false)?;
|
||||
|
||||
|
@ -148,6 +154,11 @@ impl Bundle {
|
|||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(target) = &self.build.target {
|
||||
settings = settings.target(target.to_string());
|
||||
}
|
||||
|
||||
let settings = settings.build();
|
||||
|
||||
// on macos we need to set CI=true (https://github.com/tauri-apps/tauri/issues/2567)
|
||||
|
@ -156,9 +167,9 @@ impl Bundle {
|
|||
|
||||
tauri_bundler::bundle::bundle_project(settings.unwrap()).unwrap_or_else(|err|{
|
||||
#[cfg(target_os = "macos")]
|
||||
panic!("Failed to bundle project: {}\nMake sure you have automation enabled in your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208) and full disk access enabled for your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208)", err);
|
||||
panic!("Failed to bundle project: {:#?}\nMake sure you have automation enabled in your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208) and full disk access enabled for your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208)", err);
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
panic!("Failed to bundle project: {}", err);
|
||||
panic!("Failed to bundle project: {:#?}", err);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -6,10 +6,6 @@ use super::*;
|
|||
/// Config options for the build system.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Parser)]
|
||||
pub struct ConfigOptsBuild {
|
||||
/// The index HTML file to drive the bundling process [default: index.html]
|
||||
#[arg(long)]
|
||||
pub target: Option<PathBuf>,
|
||||
|
||||
/// Build in release mode [default: false]
|
||||
#[clap(long)]
|
||||
#[serde(default)]
|
||||
|
@ -35,14 +31,18 @@ pub struct ConfigOptsBuild {
|
|||
/// Space separated list of features to activate
|
||||
#[clap(long)]
|
||||
pub features: Option<Vec<String>>,
|
||||
|
||||
/// Rustc platform triple
|
||||
#[clap(long)]
|
||||
pub target: Option<String>,
|
||||
|
||||
/// Extra arguments passed to cargo build
|
||||
#[clap(last = true)]
|
||||
pub cargo_args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Parser)]
|
||||
pub struct ConfigOptsServe {
|
||||
/// The index HTML file to drive the bundling process [default: index.html]
|
||||
#[arg(short, long)]
|
||||
pub target: Option<PathBuf>,
|
||||
|
||||
/// Port of dev server
|
||||
#[clap(long)]
|
||||
#[clap(default_value_t = 8080)]
|
||||
|
@ -89,6 +89,14 @@ pub struct ConfigOptsServe {
|
|||
/// Space separated list of features to activate
|
||||
#[clap(long)]
|
||||
pub features: Option<Vec<String>>,
|
||||
|
||||
/// Rustc platform triple
|
||||
#[clap(long)]
|
||||
pub target: Option<String>,
|
||||
|
||||
/// Extra arguments passed to cargo build
|
||||
#[clap(last = true)]
|
||||
pub cargo_args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)]
|
||||
|
@ -129,4 +137,12 @@ pub struct ConfigOptsBundle {
|
|||
/// Space separated list of features to activate
|
||||
#[clap(long)]
|
||||
pub features: Option<Vec<String>>,
|
||||
|
||||
/// Rustc platform triple
|
||||
#[clap(long)]
|
||||
pub target: Option<String>,
|
||||
|
||||
/// Extra arguments passed to cargo build
|
||||
#[clap(last = true)]
|
||||
pub cargo_args: Vec<String>,
|
||||
}
|
||||
|
|
|
@ -34,6 +34,12 @@ impl Serve {
|
|||
// Subdirectories don't work with the server
|
||||
crate_config.dioxus_config.web.app.base_path = None;
|
||||
|
||||
if let Some(target) = self.serve.target {
|
||||
crate_config.set_target(target);
|
||||
}
|
||||
|
||||
crate_config.set_cargo_args(self.serve.cargo_args);
|
||||
|
||||
let platform = self
|
||||
.serve
|
||||
.platform
|
||||
|
|
|
@ -211,6 +211,8 @@ pub struct CrateConfig {
|
|||
pub verbose: bool,
|
||||
pub custom_profile: Option<String>,
|
||||
pub features: Option<Vec<String>>,
|
||||
pub target: Option<String>,
|
||||
pub cargo_args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -278,6 +280,8 @@ impl CrateConfig {
|
|||
let verbose = false;
|
||||
let custom_profile = None;
|
||||
let features = None;
|
||||
let target = None;
|
||||
let cargo_args = vec![];
|
||||
|
||||
Ok(Self {
|
||||
out_dir,
|
||||
|
@ -294,6 +298,8 @@ impl CrateConfig {
|
|||
custom_profile,
|
||||
features,
|
||||
verbose,
|
||||
target,
|
||||
cargo_args,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -331,6 +337,16 @@ impl CrateConfig {
|
|||
self.features = Some(features);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_target(&mut self, target: String) -> &mut Self {
|
||||
self.target = Some(target);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_cargo_args(&mut self, cargo_args: Vec<String>) -> &mut Self {
|
||||
self.cargo_args = cargo_args;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
|
|
|
@ -28,7 +28,19 @@ pub fn set_up_logging() {
|
|||
message = message,
|
||||
));
|
||||
})
|
||||
.level(log::LevelFilter::Info)
|
||||
.level(match std::env::var("DIOXUS_LOG") {
|
||||
Ok(level) => match level.to_lowercase().as_str() {
|
||||
"error" => log::LevelFilter::Error,
|
||||
"warn" => log::LevelFilter::Warn,
|
||||
"info" => log::LevelFilter::Info,
|
||||
"debug" => log::LevelFilter::Debug,
|
||||
"trace" => log::LevelFilter::Trace,
|
||||
_ => {
|
||||
panic!("Invalid log level: {}", level)
|
||||
}
|
||||
},
|
||||
Err(_) => log::LevelFilter::Info,
|
||||
})
|
||||
.chain(std::io::stdout())
|
||||
.apply()
|
||||
.unwrap();
|
||||
|
|
|
@ -42,34 +42,36 @@ async fn main() -> anyhow::Result<()> {
|
|||
|
||||
set_up_logging();
|
||||
|
||||
let bin = get_bin(args.bin)?;
|
||||
let bin = get_bin(args.bin);
|
||||
|
||||
let _dioxus_config = DioxusConfig::load(Some(bin.clone()))
|
||||
if let Ok(bin) = &bin {
|
||||
let _dioxus_config = DioxusConfig::load(Some(bin.clone()))
|
||||
.map_err(|e| anyhow!("Failed to load Dioxus config because: {e}"))?
|
||||
.unwrap_or_else(|| {
|
||||
log::warn!("You appear to be creating a Dioxus project from scratch; we will use the default config");
|
||||
DioxusConfig::default()
|
||||
});
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
PluginManager::init(_dioxus_config.plugin)
|
||||
.map_err(|e| anyhow!("🚫 Plugin system initialization failed: {e}"))?;
|
||||
#[cfg(feature = "plugin")]
|
||||
PluginManager::init(_dioxus_config.plugin)
|
||||
.map_err(|e| anyhow!("🚫 Plugin system initialization failed: {e}"))?;
|
||||
}
|
||||
|
||||
match args.action {
|
||||
Translate(opts) => opts
|
||||
.translate()
|
||||
.map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)),
|
||||
|
||||
Build(opts) => opts
|
||||
.build(Some(bin.clone()))
|
||||
Build(opts) if bin.is_ok() => opts
|
||||
.build(Some(bin.unwrap().clone()))
|
||||
.map_err(|e| anyhow!("🚫 Building project failed: {}", e)),
|
||||
|
||||
Clean(opts) => opts
|
||||
.clean(Some(bin.clone()))
|
||||
Clean(opts) if bin.is_ok() => opts
|
||||
.clean(Some(bin.unwrap().clone()))
|
||||
.map_err(|e| anyhow!("🚫 Cleaning project failed: {}", e)),
|
||||
|
||||
Serve(opts) => opts
|
||||
.serve(Some(bin.clone()))
|
||||
Serve(opts) if bin.is_ok() => opts
|
||||
.serve(Some(bin.unwrap().clone()))
|
||||
.await
|
||||
.map_err(|e| anyhow!("🚫 Serving project failed: {}", e)),
|
||||
|
||||
|
@ -81,8 +83,8 @@ async fn main() -> anyhow::Result<()> {
|
|||
.config()
|
||||
.map_err(|e| anyhow!("🚫 Configuring new project failed: {}", e)),
|
||||
|
||||
Bundle(opts) => opts
|
||||
.bundle(Some(bin.clone()))
|
||||
Bundle(opts) if bin.is_ok() => opts
|
||||
.bundle(Some(bin.unwrap().clone()))
|
||||
.map_err(|e| anyhow!("🚫 Bundling project failed: {}", e)),
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
|
@ -107,5 +109,6 @@ async fn main() -> anyhow::Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(anyhow::anyhow!(bin.unwrap_err())),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,8 +43,6 @@ pub async fn startup(config: CrateConfig) -> Result<()> {
|
|||
|
||||
let hot_reload_tx = broadcast::channel(100).0;
|
||||
|
||||
clear_paths();
|
||||
|
||||
Some(HotReloadState {
|
||||
messages: hot_reload_tx.clone(),
|
||||
file_map: file_map.clone(),
|
||||
|
@ -73,6 +71,7 @@ pub async fn serve(config: CrateConfig, hot_reload_state: Option<HotReloadState>
|
|||
|
||||
move || {
|
||||
let mut current_child = currently_running_child.write().unwrap();
|
||||
log::trace!("Killing old process");
|
||||
current_child.kill()?;
|
||||
let (child, result) = start_desktop(&config)?;
|
||||
*current_child = child;
|
||||
|
@ -109,7 +108,14 @@ pub async fn serve(config: CrateConfig, hot_reload_state: Option<HotReloadState>
|
|||
}
|
||||
|
||||
async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()> {
|
||||
match LocalSocketListener::bind("@dioxusin") {
|
||||
let metadata = cargo_metadata::MetadataCommand::new()
|
||||
.no_deps()
|
||||
.exec()
|
||||
.unwrap();
|
||||
let target_dir = metadata.target_directory.as_std_path();
|
||||
let path = target_dir.join("dioxusin");
|
||||
clear_paths(&path);
|
||||
match LocalSocketListener::bind(path) {
|
||||
Ok(local_socket_stream) => {
|
||||
let aborted = Arc::new(Mutex::new(false));
|
||||
// States
|
||||
|
@ -148,7 +154,11 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
|
|||
println!("Connected to hot reloading 🚀");
|
||||
}
|
||||
Err(err) => {
|
||||
if err.kind() != std::io::ErrorKind::WouldBlock {
|
||||
let error_string = err.to_string();
|
||||
// Filter out any error messages about a operation that may block and an error message that triggers on some operating systems that says "Waiting for a process to open the other end of the pipe" without WouldBlock being set
|
||||
let display_error = err.kind() != std::io::ErrorKind::WouldBlock
|
||||
&& !error_string.contains("Waiting for a process");
|
||||
if display_error {
|
||||
println!("Error connecting to hot reloading: {} (Hot reloading is a feature of the dioxus-cli. If you are not using the CLI, this error can be ignored)", err);
|
||||
}
|
||||
}
|
||||
|
@ -181,17 +191,14 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_paths() {
|
||||
fn clear_paths(file_socket_path: &std::path::Path) {
|
||||
if cfg!(target_os = "macos") {
|
||||
// On unix, if you force quit the application, it can leave the file socket open
|
||||
// This will cause the local socket listener to fail to open
|
||||
// We check if the file socket is already open from an old session and then delete it
|
||||
let paths = ["./dioxusin", "./@dioxusin"];
|
||||
for path in paths {
|
||||
let path = std::path::PathBuf::from(path);
|
||||
if path.exists() {
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
if file_socket_path.exists() {
|
||||
let _ = std::fs::remove_file(file_socket_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -212,6 +219,7 @@ fn send_msg(msg: HotReloadMsg, channel: &mut impl std::io::Write) -> bool {
|
|||
|
||||
pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> {
|
||||
// Run the desktop application
|
||||
log::trace!("Building application");
|
||||
let result = crate::builder::build_desktop(config, true)?;
|
||||
|
||||
match &config.executable {
|
||||
|
@ -222,6 +230,7 @@ pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> {
|
|||
if cfg!(windows) {
|
||||
file.set_extension("exe");
|
||||
}
|
||||
log::trace!("Running application from {:?}", file);
|
||||
let child = Command::new(file.to_str().unwrap()).spawn()?;
|
||||
|
||||
Ok((child, result))
|
||||
|
|
|
@ -22,17 +22,20 @@ pub fn print_console_info(
|
|||
options: PrettierOptions,
|
||||
web_info: Option<WebServerInfo>,
|
||||
) {
|
||||
if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") {
|
||||
"cls"
|
||||
} else {
|
||||
"clear"
|
||||
})
|
||||
.output()
|
||||
{
|
||||
print!("{}", String::from_utf8_lossy(&native_clearseq.stdout));
|
||||
} else {
|
||||
// Try ANSI-Escape characters
|
||||
print!("\x1b[2J\x1b[H");
|
||||
// Don't clear the screen if the user has set the DIOXUS_LOG environment variable to "trace" so that we can see the logs
|
||||
if Some("trace") != std::env::var("DIOXUS_LOG").ok().as_deref() {
|
||||
if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") {
|
||||
"cls"
|
||||
} else {
|
||||
"clear"
|
||||
})
|
||||
.output()
|
||||
{
|
||||
print!("{}", String::from_utf8_lossy(&native_clearseq.stdout));
|
||||
} else {
|
||||
// Try ANSI-Escape characters
|
||||
print!("\x1b[2J\x1b[H");
|
||||
}
|
||||
}
|
||||
|
||||
let mut profile = if config.release { "Release" } else { "Debug" }.to_string();
|
||||
|
|
|
@ -11,6 +11,7 @@ use axum::{
|
|||
body::{Full, HttpBody},
|
||||
extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
|
||||
http::{
|
||||
self,
|
||||
header::{HeaderName, HeaderValue},
|
||||
Method, Response, StatusCode,
|
||||
},
|
||||
|
@ -262,7 +263,7 @@ async fn setup_router(
|
|||
.override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
|
||||
.and_then(
|
||||
move |response: Response<ServeFileSystemResponseBody>| async move {
|
||||
let response = if file_service_config
|
||||
let mut response = if file_service_config
|
||||
.dioxus_config
|
||||
.web
|
||||
.watcher
|
||||
|
@ -290,6 +291,13 @@ async fn setup_router(
|
|||
} else {
|
||||
response.map(|body| body.boxed())
|
||||
};
|
||||
let headers = response.headers_mut();
|
||||
headers.insert(
|
||||
http::header::CACHE_CONTROL,
|
||||
HeaderValue::from_static("no-cache"),
|
||||
);
|
||||
headers.insert(http::header::PRAGMA, HeaderValue::from_static("no-cache"));
|
||||
headers.insert(http::header::EXPIRES, HeaderValue::from_static("0"));
|
||||
Ok(response)
|
||||
},
|
||||
)
|
||||
|
|
|
@ -58,8 +58,11 @@ impl ToTokens for ComponentDeserializerOutput {
|
|||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let comp_fn = &self.comp_fn;
|
||||
let props_struct = &self.props_struct;
|
||||
let fn_ident = &comp_fn.sig.ident;
|
||||
|
||||
let doc = format!("Properties for the [`{fn_ident}`] component.");
|
||||
tokens.append_all(quote! {
|
||||
#[doc = #doc]
|
||||
#props_struct
|
||||
#[allow(non_snake_case)]
|
||||
#comp_fn
|
||||
|
|
|
@ -243,10 +243,6 @@ mod field_info {
|
|||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn type_from_inside_option(&self, check_option_name: bool) -> Option<&syn::Type> {
|
||||
type_from_inside_option(self.ty, check_option_name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
|
@ -783,31 +779,16 @@ Finally, call `.build()` to create the instance of `{name}`.
|
|||
None => quote!(),
|
||||
};
|
||||
|
||||
// NOTE: both auto_into and strip_option affect `arg_type` and `arg_expr`, but the order of
|
||||
// nesting is different so we have to do this little dance.
|
||||
let arg_type = if field.builder_attr.strip_option {
|
||||
field.type_from_inside_option(false).ok_or_else(|| {
|
||||
Error::new_spanned(
|
||||
field_type,
|
||||
"can't `strip_option` - field is not `Option<...>`",
|
||||
let arg_type = field_type;
|
||||
let (arg_type, arg_expr) =
|
||||
if field.builder_attr.auto_into || field.builder_attr.strip_option {
|
||||
(
|
||||
quote!(impl ::core::convert::Into<#arg_type>),
|
||||
quote!(#field_name.into()),
|
||||
)
|
||||
})?
|
||||
} else {
|
||||
field_type
|
||||
};
|
||||
let (arg_type, arg_expr) = if field.builder_attr.auto_into {
|
||||
(
|
||||
quote!(impl ::core::convert::Into<#arg_type>),
|
||||
quote!(#field_name.into()),
|
||||
)
|
||||
} else {
|
||||
(quote!(#arg_type), quote!(#field_name))
|
||||
};
|
||||
let arg_expr = if field.builder_attr.strip_option {
|
||||
quote!(Some(#arg_expr))
|
||||
} else {
|
||||
arg_expr
|
||||
};
|
||||
} else {
|
||||
(quote!(#arg_type), quote!(#field_name))
|
||||
};
|
||||
|
||||
let repeated_fields_error_type_name = syn::Ident::new(
|
||||
&format!(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::nodes::RenderReturn;
|
||||
use crate::{Attribute, AttributeValue};
|
||||
use crate::{Attribute, AttributeValue, VComponent};
|
||||
use bumpalo::Bump;
|
||||
use std::cell::RefCell;
|
||||
use std::cell::{Cell, UnsafeCell};
|
||||
|
@ -7,7 +7,10 @@ use std::cell::{Cell, UnsafeCell};
|
|||
pub(crate) struct BumpFrame {
|
||||
pub bump: UnsafeCell<Bump>,
|
||||
pub node: Cell<*const RenderReturn<'static>>,
|
||||
|
||||
// The bump allocator will not call the destructor of the objects it allocated. Attributes and props need to have there destructor called, so we keep a list of them to drop before the bump allocator is reset.
|
||||
pub(crate) attributes_to_drop_before_reset: RefCell<Vec<*const Attribute<'static>>>,
|
||||
pub(crate) props_to_drop_before_reset: RefCell<Vec<*const VComponent<'static>>>,
|
||||
}
|
||||
|
||||
impl BumpFrame {
|
||||
|
@ -17,6 +20,7 @@ impl BumpFrame {
|
|||
bump: UnsafeCell::new(bump),
|
||||
node: Cell::new(std::ptr::null()),
|
||||
attributes_to_drop_before_reset: Default::default(),
|
||||
props_to_drop_before_reset: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,6 +45,10 @@ impl BumpFrame {
|
|||
.push(attribute);
|
||||
}
|
||||
|
||||
/// Reset the bump allocator and drop all the attributes and props that were allocated in it.
|
||||
///
|
||||
/// # Safety
|
||||
/// The caller must insure that no reference to anything allocated in the bump allocator is available after this function is called.
|
||||
pub(crate) unsafe fn reset(&self) {
|
||||
let mut attributes = self.attributes_to_drop_before_reset.borrow_mut();
|
||||
attributes.drain(..).for_each(|attribute| {
|
||||
|
@ -49,9 +57,20 @@ impl BumpFrame {
|
|||
_ = l.take();
|
||||
}
|
||||
});
|
||||
let mut props = self.props_to_drop_before_reset.borrow_mut();
|
||||
props.drain(..).for_each(|prop| {
|
||||
let prop = unsafe { &*prop };
|
||||
_ = prop.props.borrow_mut().take();
|
||||
});
|
||||
unsafe {
|
||||
let bump = &mut *self.bump.get();
|
||||
bump.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for BumpFrame {
|
||||
fn drop(&mut self) {
|
||||
unsafe { self.reset() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
///
|
||||
|
@ -405,6 +405,7 @@ impl<'b> VirtualDom {
|
|||
#[allow(unused_mut)]
|
||||
pub(crate) fn register_template(&mut self, mut template: Template<'static>) {
|
||||
let (path, byte_index) = template.name.rsplit_once(':').unwrap();
|
||||
|
||||
let byte_index = byte_index.parse::<usize>().unwrap();
|
||||
// First, check if we've already seen this template
|
||||
if self
|
||||
|
|
|
@ -560,7 +560,7 @@ impl<'b> VirtualDom {
|
|||
// If none of the old keys are reused by the new children, then we remove all the remaining old children and
|
||||
// create the new children afresh.
|
||||
if shared_keys.is_empty() {
|
||||
if old.get(0).is_some() {
|
||||
if old.first().is_some() {
|
||||
self.remove_nodes(&old[1..]);
|
||||
self.replace(&old[0], new);
|
||||
} else {
|
||||
|
|
|
@ -18,7 +18,7 @@ use crate::{innerlude::VNode, ScopeState};
|
|||
|
||||
/// A concrete type provider for closures that build [`VNode`] structures.
|
||||
///
|
||||
/// This struct wraps lazy structs that build [`VNode`] trees Normally, we cannot perform a blanket implementation over
|
||||
/// This struct wraps lazy structs that build [`VNode`] trees. Normally, we cannot perform a blanket implementation over
|
||||
/// closures, but if we wrap the closure in a concrete type, we can use it for different branches in matching.
|
||||
///
|
||||
///
|
||||
|
|
|
@ -707,7 +707,7 @@ impl<'a, 'b> IntoDynNode<'b> for &'a str {
|
|||
impl IntoDynNode<'_> for String {
|
||||
fn into_vnode(self, cx: &ScopeState) -> DynamicNode {
|
||||
DynamicNode::Text(VText {
|
||||
value: cx.bump().alloc(self),
|
||||
value: cx.bump().alloc_str(&self),
|
||||
id: Default::default(),
|
||||
})
|
||||
}
|
||||
|
@ -791,6 +791,12 @@ impl<'a> IntoAttributeValue<'a> for &'a str {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoAttributeValue<'a> for String {
|
||||
fn into_value(self, cx: &'a Bump) -> AttributeValue<'a> {
|
||||
AttributeValue::Text(cx.alloc_str(&self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoAttributeValue<'a> for f64 {
|
||||
fn into_value(self, _: &'a Bump) -> AttributeValue<'a> {
|
||||
AttributeValue::Float(self)
|
||||
|
|
|
@ -230,17 +230,7 @@ impl ScopeContext {
|
|||
/// This is good for tasks that need to be run after the component has been dropped.
|
||||
pub fn spawn_forever(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
|
||||
// The root scope will never be unmounted so we can just add the task at the top of the app
|
||||
let id = self.tasks.spawn(ScopeId::ROOT, fut);
|
||||
|
||||
// wake up the scheduler if it is sleeping
|
||||
self.tasks
|
||||
.sender
|
||||
.unbounded_send(SchedulerMsg::TaskNotified(id))
|
||||
.expect("Scheduler should exist");
|
||||
|
||||
self.spawned_tasks.borrow_mut().insert(id);
|
||||
|
||||
id
|
||||
self.tasks.spawn(ScopeId::ROOT, fut)
|
||||
}
|
||||
|
||||
/// Informs the scheduler that this task is no longer needed and should be removed.
|
||||
|
|
|
@ -367,12 +367,17 @@ impl<'src> ScopeState {
|
|||
}
|
||||
|
||||
let mut props = self.borrowed_props.borrow_mut();
|
||||
let mut drop_props = self
|
||||
.previous_frame()
|
||||
.props_to_drop_before_reset
|
||||
.borrow_mut();
|
||||
for node in element.dynamic_nodes {
|
||||
if let DynamicNode::Component(comp) = node {
|
||||
let unbounded = unsafe { std::mem::transmute(comp as *const VComponent) };
|
||||
if !comp.static_props {
|
||||
let unbounded = unsafe { std::mem::transmute(comp as *const VComponent) };
|
||||
props.push(unbounded);
|
||||
}
|
||||
drop_props.push(unbounded);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,8 +18,8 @@ dioxus-hot-reload = { workspace = true, optional = true }
|
|||
serde = "1.0.136"
|
||||
serde_json = "1.0.79"
|
||||
thiserror = { workspace = true }
|
||||
wry = { version = "0.28.0", default-features = false, features = ["protocol", "file-drop"] }
|
||||
tracing = { workspace = true }
|
||||
wry = { version = "0.34.0", default-features = false, features = ["tao", "protocol", "file-drop"] }
|
||||
futures-channel = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
"sync",
|
||||
|
@ -37,10 +37,12 @@ slab = { workspace = true }
|
|||
futures-util = { workspace = true }
|
||||
urlencoding = "2.1.2"
|
||||
async-trait = "0.1.68"
|
||||
crossbeam-channel = "0.5.8"
|
||||
|
||||
|
||||
[target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies]
|
||||
rfd = "0.11.3"
|
||||
rfd = "0.12"
|
||||
global-hotkey = { git = "https://github.com/tauri-apps/global-hotkey" }
|
||||
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
objc = "0.2.7"
|
||||
|
@ -56,9 +58,8 @@ tokio_runtime = ["tokio"]
|
|||
fullscreen = ["wry/fullscreen"]
|
||||
transparent = ["wry/transparent"]
|
||||
devtools = ["wry/devtools"]
|
||||
tray = ["wry/tray"]
|
||||
dox = ["wry/dox"]
|
||||
hot-reload = ["dioxus-hot-reload"]
|
||||
gnu = []
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
default-features = false
|
||||
|
|
9
packages/desktop/build.rs
Normal file
9
packages/desktop/build.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
fn main() {
|
||||
// WARN about wry support on windows gnu targets. GNU windows targets don't work well in wry currently
|
||||
if std::env::var("CARGO_CFG_WINDOWS").is_ok()
|
||||
&& std::env::var("CARGO_CFG_TARGET_ENV").unwrap() == "gnu"
|
||||
&& !cfg!(feature = "gnu")
|
||||
{
|
||||
println!("cargo:warning=GNU windows targets have some limitations within Wry. Using the MSVC windows toolchain is recommended. If you would like to use continue using GNU, you can read https://github.com/wravery/webview2-rs#cross-compilation and disable this warning by adding the gnu feature to dioxus-desktop in your Cargo.toml")
|
||||
}
|
||||
}
|
|
@ -229,75 +229,111 @@ fn app(cx: Scope) -> Element {
|
|||
println!("{:?}", event.data);
|
||||
assert!(event.data.modifiers().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)
|
||||
},
|
||||
}
|
||||
}
|
||||
div {
|
||||
id: "mouse_move_div",
|
||||
onmousemove: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
div {
|
||||
id: "mouse_click_div",
|
||||
onclick: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
assert!(event.data.modifiers().is_empty());
|
||||
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_dblclick_div",
|
||||
ondblclick: 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));
|
||||
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{
|
||||
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",
|
||||
onmousedown: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
assert!(event.data.modifiers().is_empty());
|
||||
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));
|
||||
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{
|
||||
div {
|
||||
id: "mouse_up_div",
|
||||
onmouseup: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
assert!(event.data.modifiers().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)
|
||||
}
|
||||
}
|
||||
div{
|
||||
div {
|
||||
id: "wheel_div",
|
||||
width: "100px",
|
||||
height: "100px",
|
||||
background_color: "red",
|
||||
onwheel: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
let dioxus_html::geometry::WheelDelta::Pixels(delta)= event.data.delta()else{
|
||||
panic!("Expected delta to be in pixels")
|
||||
};
|
||||
let dioxus_html::geometry::WheelDelta::Pixels(delta) = event.data.delta() else {
|
||||
panic!("Expected delta to be in pixels") };
|
||||
assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0));
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
input{
|
||||
input {
|
||||
id: "key_down_div",
|
||||
onkeydown: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
|
@ -306,11 +342,10 @@ fn app(cx: Scope) -> Element {
|
|||
assert_eq!(event.data.code().to_string(), "KeyA");
|
||||
assert_eq!(event.data.location, 0);
|
||||
assert!(event.data.is_auto_repeating());
|
||||
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
input{
|
||||
input {
|
||||
id: "key_up_div",
|
||||
onkeyup: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
|
@ -319,11 +354,10 @@ fn app(cx: Scope) -> Element {
|
|||
assert_eq!(event.data.code().to_string(), "KeyA");
|
||||
assert_eq!(event.data.location, 0);
|
||||
assert!(!event.data.is_auto_repeating());
|
||||
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
input{
|
||||
input {
|
||||
id: "key_press_div",
|
||||
onkeypress: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
|
@ -332,18 +366,17 @@ fn app(cx: Scope) -> Element {
|
|||
assert_eq!(event.data.code().to_string(), "KeyA");
|
||||
assert_eq!(event.data.location, 0);
|
||||
assert!(!event.data.is_auto_repeating());
|
||||
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
input{
|
||||
input {
|
||||
id: "focus_in_div",
|
||||
onfocusin: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
recieved_events.modify(|x| *x + 1)
|
||||
}
|
||||
}
|
||||
input{
|
||||
input {
|
||||
id: "focus_out_div",
|
||||
onfocusout: move |event| {
|
||||
println!("{:?}", event.data);
|
||||
|
|
|
@ -4,10 +4,11 @@ use std::rc::Weak;
|
|||
|
||||
use crate::create_new_window;
|
||||
use crate::events::IpcMessage;
|
||||
use crate::protocol::AssetFuture;
|
||||
use crate::protocol::AssetHandlerRegistry;
|
||||
use crate::query::QueryEngine;
|
||||
use crate::shortcut::ShortcutId;
|
||||
use crate::shortcut::ShortcutRegistry;
|
||||
use crate::shortcut::ShortcutRegistryError;
|
||||
use crate::shortcut::{HotKey, ShortcutId, ShortcutRegistry, ShortcutRegistryError};
|
||||
use crate::AssetHandler;
|
||||
use crate::Config;
|
||||
use crate::WebviewHandler;
|
||||
use dioxus_core::ScopeState;
|
||||
|
@ -15,7 +16,6 @@ use dioxus_core::VirtualDom;
|
|||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
use dioxus_hot_reload::HotReloadMsg;
|
||||
use slab::Slab;
|
||||
use wry::application::accelerator::Accelerator;
|
||||
use wry::application::event::Event;
|
||||
use wry::application::event_loop::EventLoopProxy;
|
||||
use wry::application::event_loop::EventLoopWindowTarget;
|
||||
|
@ -67,6 +67,8 @@ pub struct DesktopService {
|
|||
|
||||
pub(crate) shortcut_manager: ShortcutRegistry,
|
||||
|
||||
pub(crate) asset_handlers: AssetHandlerRegistry,
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
pub(crate) views: Rc<RefCell<Vec<*mut objc::runtime::Object>>>,
|
||||
}
|
||||
|
@ -91,6 +93,7 @@ impl DesktopService {
|
|||
webviews: WebviewQueue,
|
||||
event_handlers: WindowEventHandlers,
|
||||
shortcut_manager: ShortcutRegistry,
|
||||
asset_handlers: AssetHandlerRegistry,
|
||||
) -> Self {
|
||||
Self {
|
||||
webview: Rc::new(webview),
|
||||
|
@ -100,6 +103,7 @@ impl DesktopService {
|
|||
pending_windows: webviews,
|
||||
event_handlers,
|
||||
shortcut_manager,
|
||||
asset_handlers,
|
||||
#[cfg(target_os = "ios")]
|
||||
views: Default::default(),
|
||||
}
|
||||
|
@ -233,11 +237,11 @@ impl DesktopService {
|
|||
/// Linux: Only works on x11. See [this issue](https://github.com/tauri-apps/tao/issues/331) for more information.
|
||||
pub fn create_shortcut(
|
||||
&self,
|
||||
accelerator: Accelerator,
|
||||
hotkey: HotKey,
|
||||
callback: impl FnMut() + 'static,
|
||||
) -> Result<ShortcutId, ShortcutRegistryError> {
|
||||
self.shortcut_manager
|
||||
.add_shortcut(accelerator, Box::new(callback))
|
||||
.add_shortcut(hotkey, Box::new(callback))
|
||||
}
|
||||
|
||||
/// Remove a global shortcut
|
||||
|
@ -250,6 +254,20 @@ impl DesktopService {
|
|||
self.shortcut_manager.remove_all()
|
||||
}
|
||||
|
||||
/// Provide a callback to handle asset loading yourself.
|
||||
///
|
||||
/// See [`use_asset_handle`](crate::use_asset_handle) for a convenient hook.
|
||||
pub async fn register_asset_handler<F: AssetFuture>(&self, f: impl AssetHandler<F>) -> usize {
|
||||
self.asset_handlers.register_handler(f).await
|
||||
}
|
||||
|
||||
/// Removes an asset handler by its identifier.
|
||||
///
|
||||
/// Returns `None` if the handler did not exist.
|
||||
pub async fn remove_asset_handler(&self, id: usize) -> Option<()> {
|
||||
self.asset_handlers.remove_handler(id).await
|
||||
}
|
||||
|
||||
/// Push an objc view to the window
|
||||
#[cfg(target_os = "ios")]
|
||||
pub fn push_view(&self, view: objc_id::ShareId<objc::runtime::Object>) {
|
||||
|
@ -369,17 +387,10 @@ impl WryWindowEventHandlerInner {
|
|||
target: &EventLoopWindowTarget<UserWindowEvent>,
|
||||
) {
|
||||
// if this event does not apply to the window this listener cares about, return
|
||||
match event {
|
||||
Event::WindowEvent { window_id, .. }
|
||||
| Event::MenuEvent {
|
||||
window_id: Some(window_id),
|
||||
..
|
||||
} => {
|
||||
if *window_id != self.window_id {
|
||||
return;
|
||||
}
|
||||
if let Event::WindowEvent { window_id, .. } = event {
|
||||
if *window_id != self.window_id {
|
||||
return;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
(self.handler)(event, target)
|
||||
}
|
||||
|
|
|
@ -10,16 +10,16 @@ mod escape;
|
|||
mod eval;
|
||||
mod events;
|
||||
mod file_upload;
|
||||
#[cfg(any(target_os = "ios", target_os = "android"))]
|
||||
mod mobile_shortcut;
|
||||
mod protocol;
|
||||
mod query;
|
||||
mod shortcut;
|
||||
mod waker;
|
||||
mod webview;
|
||||
|
||||
#[cfg(any(target_os = "ios", target_os = "android"))]
|
||||
mod mobile_shortcut;
|
||||
|
||||
use crate::query::QueryResult;
|
||||
use crate::shortcut::GlobalHotKeyEvent;
|
||||
pub use cfg::{Config, WindowCloseBehaviour};
|
||||
pub use desktop_context::DesktopContext;
|
||||
pub use desktop_context::{
|
||||
|
@ -32,6 +32,7 @@ use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent};
|
|||
use element::DesktopElement;
|
||||
use eval::init_eval;
|
||||
use futures_util::{pin_mut, FutureExt};
|
||||
pub use protocol::{use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse};
|
||||
use shortcut::ShortcutRegistry;
|
||||
pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
|
||||
use std::cell::Cell;
|
||||
|
@ -43,11 +44,12 @@ use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
|
|||
pub use tao::window::WindowBuilder;
|
||||
use tao::{
|
||||
event::{Event, StartCause, WindowEvent},
|
||||
event_loop::{ControlFlow, EventLoop},
|
||||
event_loop::ControlFlow,
|
||||
};
|
||||
pub use webview::build_default_menu_bar;
|
||||
pub use wry;
|
||||
pub use wry::application as tao;
|
||||
use wry::application::event_loop::EventLoopBuilder;
|
||||
use wry::webview::WebView;
|
||||
use wry::{application::window::WindowId, webview::WebContext};
|
||||
|
||||
|
@ -121,7 +123,7 @@ pub fn launch_cfg(root: Component, config_builder: Config) {
|
|||
/// }
|
||||
/// ```
|
||||
pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config) {
|
||||
let event_loop = EventLoop::<UserWindowEvent>::with_user_event();
|
||||
let event_loop = EventLoopBuilder::<UserWindowEvent>::with_user_event().build();
|
||||
|
||||
let proxy = event_loop.create_proxy();
|
||||
|
||||
|
@ -158,7 +160,8 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
|||
|
||||
let queue = WebviewQueue::default();
|
||||
|
||||
let shortcut_manager = ShortcutRegistry::new(&event_loop);
|
||||
let shortcut_manager = ShortcutRegistry::new();
|
||||
let global_hotkey_channel = GlobalHotKeyEvent::receiver();
|
||||
|
||||
// move the props into a cell so we can pop it out later to create the first window
|
||||
// iOS panics if we create a window before the event loop is started
|
||||
|
@ -167,10 +170,14 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
|||
let mut is_visible_before_start = true;
|
||||
|
||||
event_loop.run(move |window_event, event_loop, control_flow| {
|
||||
*control_flow = ControlFlow::Wait;
|
||||
*control_flow = ControlFlow::Poll;
|
||||
|
||||
event_handlers.apply_event(&window_event, event_loop);
|
||||
|
||||
if let Ok(event) = global_hotkey_channel.try_recv() {
|
||||
shortcut_manager.call_handlers(event);
|
||||
}
|
||||
|
||||
match window_event {
|
||||
Event::WindowEvent {
|
||||
event, window_id, ..
|
||||
|
@ -376,7 +383,6 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
|||
|
||||
_ => {}
|
||||
},
|
||||
Event::GlobalShortcutEvent(id) => shortcut_manager.call_handlers(id),
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
|
@ -391,7 +397,8 @@ fn create_new_window(
|
|||
event_handlers: &WindowEventHandlers,
|
||||
shortcut_manager: ShortcutRegistry,
|
||||
) -> 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(
|
||||
webview,
|
||||
proxy.clone(),
|
||||
|
@ -399,6 +406,7 @@ fn create_new_window(
|
|||
queue.clone(),
|
||||
event_handlers.clone(),
|
||||
shortcut_manager,
|
||||
asset_handlers,
|
||||
));
|
||||
|
||||
let cx = dom.base_scope();
|
||||
|
|
|
@ -1,29 +1,51 @@
|
|||
#![allow(unused)]
|
||||
|
||||
use super::*;
|
||||
use wry::application::accelerator::Accelerator;
|
||||
use std::str::FromStr;
|
||||
use wry::application::event_loop::EventLoopWindowTarget;
|
||||
|
||||
pub struct GlobalShortcut();
|
||||
pub struct ShortcutManager();
|
||||
use dioxus_html::input_data::keyboard_types::Modifiers;
|
||||
|
||||
impl ShortcutManager {
|
||||
pub fn new<T>(target: &EventLoopWindowTarget<T>) -> Self {
|
||||
Self()
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Accelerator;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct HotKey;
|
||||
|
||||
impl HotKey {
|
||||
pub fn new(mods: Option<Modifiers>, key: Code) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn register(
|
||||
&mut self,
|
||||
accelerator: Accelerator,
|
||||
) -> Result<GlobalShortcut, ShortcutManagerError> {
|
||||
Ok(GlobalShortcut())
|
||||
pub fn id(&self) -> u32 {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for HotKey {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(HotKey)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GlobalHotKeyManager();
|
||||
|
||||
impl GlobalHotKeyManager {
|
||||
pub fn new() -> Result<Self, HotkeyError> {
|
||||
Ok(Self())
|
||||
}
|
||||
|
||||
pub fn unregister(&mut self, id: ShortcutId) -> Result<(), ShortcutManagerError> {
|
||||
pub fn register(&mut self, accelerator: HotKey) -> Result<HotKey, HotkeyError> {
|
||||
Ok(HotKey)
|
||||
}
|
||||
|
||||
pub fn unregister(&mut self, id: HotKey) -> Result<(), HotkeyError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn unregister_all(&mut self) -> Result<(), ShortcutManagerError> {
|
||||
pub fn unregister_all(&mut self, _: &[HotKey]) -> Result<(), HotkeyError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -33,23 +55,35 @@ use std::{error, fmt};
|
|||
/// An error whose cause the `ShortcutManager` to fail.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug)]
|
||||
pub enum ShortcutManagerError {
|
||||
pub enum HotkeyError {
|
||||
AcceleratorAlreadyRegistered(Accelerator),
|
||||
AcceleratorNotRegistered(Accelerator),
|
||||
InvalidAccelerator(String),
|
||||
HotKeyParseError(String),
|
||||
}
|
||||
|
||||
impl error::Error for ShortcutManagerError {}
|
||||
impl fmt::Display for ShortcutManagerError {
|
||||
impl error::Error for HotkeyError {}
|
||||
impl fmt::Display for HotkeyError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
match self {
|
||||
ShortcutManagerError::AcceleratorAlreadyRegistered(e) => {
|
||||
HotkeyError::AcceleratorAlreadyRegistered(e) => {
|
||||
f.pad(&format!("hotkey already registered: {:?}", e))
|
||||
}
|
||||
ShortcutManagerError::AcceleratorNotRegistered(e) => {
|
||||
HotkeyError::AcceleratorNotRegistered(e) => {
|
||||
f.pad(&format!("hotkey not registered: {:?}", e))
|
||||
}
|
||||
ShortcutManagerError::InvalidAccelerator(e) => e.fmt(f),
|
||||
HotkeyError::HotKeyParseError(e) => e.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GlobalHotKeyEvent {
|
||||
pub id: u32,
|
||||
}
|
||||
|
||||
impl GlobalHotKeyEvent {
|
||||
pub fn receiver() -> crossbeam_channel::Receiver<GlobalHotKeyEvent> {
|
||||
crossbeam_channel::unbounded().1
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type Code = dioxus_html::input_data::keyboard_types::Code;
|
||||
|
|
|
@ -1,13 +1,26 @@
|
|||
use dioxus_core::ScopeState;
|
||||
use dioxus_interpreter_js::{COMMON_JS, INTERPRETER_JS};
|
||||
use slab::Slab;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
future::Future,
|
||||
ops::Deref,
|
||||
path::{Path, PathBuf},
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{
|
||||
runtime::Handle,
|
||||
sync::{OnceCell, RwLock},
|
||||
};
|
||||
use wry::{
|
||||
http::{status::StatusCode, Request, Response},
|
||||
Result,
|
||||
};
|
||||
|
||||
use crate::{use_window, DesktopContext};
|
||||
|
||||
fn module_loader(root_name: &str) -> String {
|
||||
let js = INTERPRETER_JS.replace(
|
||||
"/*POST_HANDLE_EDITS*/",
|
||||
|
@ -51,12 +64,156 @@ fn module_loader(root_name: &str) -> String {
|
|||
)
|
||||
}
|
||||
|
||||
pub(super) fn desktop_handler(
|
||||
request: &Request<Vec<u8>>,
|
||||
/// An arbitrary asset is an HTTP response containing a binary body.
|
||||
pub type AssetResponse = Response<Cow<'static, [u8]>>;
|
||||
|
||||
/// A future that returns an [`AssetResponse`]. This future may be spawned in a new thread,
|
||||
/// so it must be [`Send`], [`Sync`], and `'static`.
|
||||
pub trait AssetFuture: Future<Output = Option<AssetResponse>> + Send + Sync + 'static {}
|
||||
impl<T: Future<Output = Option<AssetResponse>> + Send + Sync + 'static> AssetFuture for T {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// A request for an asset. This is a wrapper around [`Request<Vec<u8>>`] that provides methods specific to asset requests.
|
||||
pub struct AssetRequest {
|
||||
path: PathBuf,
|
||||
request: Arc<Request<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl AssetRequest {
|
||||
/// Get the path the asset request is for
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Request<Vec<u8>>> for AssetRequest {
|
||||
fn from(request: Request<Vec<u8>>) -> Self {
|
||||
let decoded = urlencoding::decode(request.uri().path().trim_start_matches('/'))
|
||||
.expect("expected URL to be UTF-8 encoded");
|
||||
let path = PathBuf::from(&*decoded);
|
||||
Self {
|
||||
request: Arc::new(request),
|
||||
path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for AssetRequest {
|
||||
type Target = Request<Vec<u8>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.request
|
||||
}
|
||||
}
|
||||
|
||||
/// A handler that takes an [`AssetRequest`] and returns a future that either loads the asset, or returns `None`.
|
||||
/// This handler is stashed indefinitely in a context object, so it must be `'static`.
|
||||
pub trait AssetHandler<F: AssetFuture>: Send + Sync + 'static {
|
||||
/// Handle an asset request, returning a future that either loads the asset, or returns `None`
|
||||
fn handle_request(&self, request: &AssetRequest) -> F;
|
||||
}
|
||||
|
||||
impl<F: AssetFuture, T: Fn(&AssetRequest) -> F + Send + Sync + 'static> AssetHandler<F> for T {
|
||||
fn handle_request(&self, request: &AssetRequest) -> F {
|
||||
self(request)
|
||||
}
|
||||
}
|
||||
|
||||
type AssetHandlerRegistryInner =
|
||||
Slab<Box<dyn Fn(&AssetRequest) -> Pin<Box<dyn AssetFuture>> + Send + Sync + 'static>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AssetHandlerRegistry(Arc<RwLock<AssetHandlerRegistryInner>>);
|
||||
|
||||
impl AssetHandlerRegistry {
|
||||
pub fn new() -> Self {
|
||||
AssetHandlerRegistry(Arc::new(RwLock::new(Slab::new())))
|
||||
}
|
||||
|
||||
pub async fn register_handler<F: AssetFuture>(&self, f: impl AssetHandler<F>) -> usize {
|
||||
let mut registry = self.0.write().await;
|
||||
registry.insert(Box::new(move |req| Box::pin(f.handle_request(req))))
|
||||
}
|
||||
|
||||
pub async fn remove_handler(&self, id: usize) -> Option<()> {
|
||||
let mut registry = self.0.write().await;
|
||||
registry.try_remove(id).map(|_| ())
|
||||
}
|
||||
|
||||
pub async fn try_handlers(&self, req: &AssetRequest) -> Option<AssetResponse> {
|
||||
let registry = self.0.read().await;
|
||||
for (_, handler) in registry.iter() {
|
||||
if let Some(response) = handler(req).await {
|
||||
return Some(response);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle to a registered asset handler.
|
||||
pub struct AssetHandlerHandle {
|
||||
desktop: DesktopContext,
|
||||
handler_id: Rc<OnceCell<usize>>,
|
||||
}
|
||||
|
||||
impl AssetHandlerHandle {
|
||||
/// Returns the ID for this handle.
|
||||
///
|
||||
/// Because registering an ID is asynchronous, this may return `None` if the
|
||||
/// registration has not completed yet.
|
||||
pub fn handler_id(&self) -> Option<usize> {
|
||||
self.handler_id.get().copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AssetHandlerHandle {
|
||||
fn drop(&mut self) {
|
||||
let cell = Rc::clone(&self.handler_id);
|
||||
let desktop = Rc::clone(&self.desktop);
|
||||
tokio::task::block_in_place(move || {
|
||||
Handle::current().block_on(async move {
|
||||
if let Some(id) = cell.get() {
|
||||
desktop.asset_handlers.remove_handler(*id).await;
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Provide a callback to handle asset loading yourself.
|
||||
///
|
||||
/// The callback takes a path as requested by the web view, and it should return `Some(response)`
|
||||
/// if you want to load the asset, and `None` if you want to fallback on the default behavior.
|
||||
pub fn use_asset_handler<F: AssetFuture>(
|
||||
cx: &ScopeState,
|
||||
handler: impl AssetHandler<F>,
|
||||
) -> &AssetHandlerHandle {
|
||||
let desktop = Rc::clone(use_window(cx));
|
||||
cx.use_hook(|| {
|
||||
let handler_id = Rc::new(OnceCell::new());
|
||||
let handler_id_ref = Rc::clone(&handler_id);
|
||||
let desktop_ref = Rc::clone(&desktop);
|
||||
cx.push_future(async move {
|
||||
let id = desktop.asset_handlers.register_handler(handler).await;
|
||||
handler_id.set(id).unwrap();
|
||||
});
|
||||
AssetHandlerHandle {
|
||||
desktop: desktop_ref,
|
||||
handler_id: handler_id_ref,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn desktop_handler(
|
||||
request: Request<Vec<u8>>,
|
||||
custom_head: Option<String>,
|
||||
custom_index: Option<String>,
|
||||
root_name: &str,
|
||||
) -> 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 request.uri().path() == "/" {
|
||||
// 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);
|
||||
}
|
||||
|
||||
// If the user provided a custom asset handler, then call it and return the response
|
||||
// if the request was handled.
|
||||
if let Some(response) = asset_handlers.try_handlers(&request).await {
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
// Else, try to serve a file from the filesystem.
|
||||
let decoded = urlencoding::decode(request.uri().path().trim_start_matches('/'))
|
||||
.expect("expected URL to be UTF-8 encoded");
|
||||
let path = PathBuf::from(&*decoded);
|
||||
|
||||
// If the path is relative, we'll try to serve it from the assets directory.
|
||||
let mut asset = get_asset_root()
|
||||
.unwrap_or_else(|| Path::new(".").to_path_buf())
|
||||
.join(&path);
|
||||
.join(&request.path);
|
||||
|
||||
if !asset.exists() {
|
||||
asset = PathBuf::from("/").join(path);
|
||||
asset = PathBuf::from("/").join(request.path);
|
||||
}
|
||||
|
||||
if asset.exists() {
|
||||
|
|
|
@ -3,11 +3,7 @@ use std::{cell::RefCell, collections::HashMap, rc::Rc, str::FromStr};
|
|||
use dioxus_core::ScopeState;
|
||||
use dioxus_html::input_data::keyboard_types::Modifiers;
|
||||
use slab::Slab;
|
||||
use wry::application::{
|
||||
accelerator::{Accelerator, AcceleratorId},
|
||||
event_loop::EventLoopWindowTarget,
|
||||
keyboard::{KeyCode, ModifiersState},
|
||||
};
|
||||
use wry::application::keyboard::ModifiersState;
|
||||
|
||||
use crate::{desktop_context::DesktopContext, use_window};
|
||||
|
||||
|
@ -20,22 +16,25 @@ use crate::{desktop_context::DesktopContext, use_window};
|
|||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
use wry::application::global_shortcut::{GlobalShortcut, ShortcutManager, ShortcutManagerError};
|
||||
pub use global_hotkey::{
|
||||
hotkey::{Code, HotKey},
|
||||
Error as HotkeyError, GlobalHotKeyEvent, GlobalHotKeyManager,
|
||||
};
|
||||
|
||||
#[cfg(any(target_os = "ios", target_os = "android"))]
|
||||
pub use crate::mobile_shortcut::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ShortcutRegistry {
|
||||
manager: Rc<RefCell<ShortcutManager>>,
|
||||
manager: Rc<RefCell<GlobalHotKeyManager>>,
|
||||
shortcuts: ShortcutMap,
|
||||
}
|
||||
|
||||
type ShortcutMap = Rc<RefCell<HashMap<AcceleratorId, Shortcut>>>;
|
||||
type ShortcutMap = Rc<RefCell<HashMap<u32, Shortcut>>>;
|
||||
|
||||
struct Shortcut {
|
||||
#[allow(unused)]
|
||||
shortcut: GlobalShortcut,
|
||||
shortcut: HotKey,
|
||||
callbacks: Slab<Box<dyn FnMut()>>,
|
||||
}
|
||||
|
||||
|
@ -54,15 +53,15 @@ impl Shortcut {
|
|||
}
|
||||
|
||||
impl ShortcutRegistry {
|
||||
pub fn new<T>(target: &EventLoopWindowTarget<T>) -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
manager: Rc::new(RefCell::new(ShortcutManager::new(target))),
|
||||
manager: Rc::new(RefCell::new(GlobalHotKeyManager::new().unwrap())),
|
||||
shortcuts: Rc::new(RefCell::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn call_handlers(&self, id: AcceleratorId) {
|
||||
if let Some(Shortcut { callbacks, .. }) = self.shortcuts.borrow_mut().get_mut(&id) {
|
||||
pub(crate) fn call_handlers(&self, id: GlobalHotKeyEvent) {
|
||||
if let Some(Shortcut { callbacks, .. }) = self.shortcuts.borrow_mut().get_mut(&id.id) {
|
||||
for (_, callback) in callbacks.iter_mut() {
|
||||
(callback)();
|
||||
}
|
||||
|
@ -71,10 +70,10 @@ impl ShortcutRegistry {
|
|||
|
||||
pub(crate) fn add_shortcut(
|
||||
&self,
|
||||
accelerator: Accelerator,
|
||||
hotkey: HotKey,
|
||||
callback: Box<dyn FnMut()>,
|
||||
) -> Result<ShortcutId, ShortcutRegistryError> {
|
||||
let accelerator_id = accelerator.clone().id();
|
||||
let accelerator_id = hotkey.clone().id();
|
||||
let mut shortcuts = self.shortcuts.borrow_mut();
|
||||
Ok(
|
||||
if let Some(callbacks) = shortcuts.get_mut(&accelerator_id) {
|
||||
|
@ -84,12 +83,12 @@ impl ShortcutRegistry {
|
|||
number: id,
|
||||
}
|
||||
} else {
|
||||
match self.manager.borrow_mut().register(accelerator) {
|
||||
Ok(global_shortcut) => {
|
||||
match self.manager.borrow_mut().register(hotkey) {
|
||||
Ok(_) => {
|
||||
let mut slab = Slab::new();
|
||||
let id = slab.insert(callback);
|
||||
let shortcut = Shortcut {
|
||||
shortcut: global_shortcut,
|
||||
shortcut: hotkey,
|
||||
callbacks: slab,
|
||||
};
|
||||
shortcuts.insert(accelerator_id, shortcut);
|
||||
|
@ -98,7 +97,7 @@ impl ShortcutRegistry {
|
|||
number: id,
|
||||
}
|
||||
}
|
||||
Err(ShortcutManagerError::InvalidAccelerator(shortcut)) => {
|
||||
Err(HotkeyError::HotKeyParseError(shortcut)) => {
|
||||
return Err(ShortcutRegistryError::InvalidShortcut(shortcut))
|
||||
}
|
||||
Err(err) => return Err(ShortcutRegistryError::Other(Box::new(err))),
|
||||
|
@ -113,15 +112,6 @@ impl ShortcutRegistry {
|
|||
callbacks.remove(id.number);
|
||||
if callbacks.is_empty() {
|
||||
if let Some(_shortcut) = shortcuts.remove(&id.id) {
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
target_os = "macos",
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
let _ = self.manager.borrow_mut().unregister(_shortcut.shortcut);
|
||||
}
|
||||
}
|
||||
|
@ -130,8 +120,8 @@ impl ShortcutRegistry {
|
|||
|
||||
pub(crate) fn remove_all(&self) {
|
||||
let mut shortcuts = self.shortcuts.borrow_mut();
|
||||
shortcuts.clear();
|
||||
let _ = self.manager.borrow_mut().unregister_all();
|
||||
let hotkeys: Vec<_> = shortcuts.drain().map(|(_, v)| v.shortcut).collect();
|
||||
let _ = self.manager.borrow_mut().unregister_all(&hotkeys);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -148,7 +138,7 @@ pub enum ShortcutRegistryError {
|
|||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
/// An global id for a shortcut.
|
||||
pub struct ShortcutId {
|
||||
id: AcceleratorId,
|
||||
id: u32,
|
||||
number: usize,
|
||||
}
|
||||
|
||||
|
@ -160,30 +150,30 @@ pub struct ShortcutHandle {
|
|||
}
|
||||
|
||||
pub trait IntoAccelerator {
|
||||
fn accelerator(&self) -> Accelerator;
|
||||
fn accelerator(&self) -> HotKey;
|
||||
}
|
||||
|
||||
impl IntoAccelerator for (dioxus_html::KeyCode, ModifiersState) {
|
||||
fn accelerator(&self) -> Accelerator {
|
||||
Accelerator::new(Some(self.1), self.0.into_key_code())
|
||||
fn accelerator(&self) -> HotKey {
|
||||
HotKey::new(Some(self.1.into_modifiers_state()), self.0.into_key_code())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAccelerator for (ModifiersState, dioxus_html::KeyCode) {
|
||||
fn accelerator(&self) -> Accelerator {
|
||||
Accelerator::new(Some(self.0), self.1.into_key_code())
|
||||
fn accelerator(&self) -> HotKey {
|
||||
HotKey::new(Some(self.0.into_modifiers_state()), self.1.into_key_code())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAccelerator for dioxus_html::KeyCode {
|
||||
fn accelerator(&self) -> Accelerator {
|
||||
Accelerator::new(None, self.into_key_code())
|
||||
fn accelerator(&self) -> HotKey {
|
||||
HotKey::new(None, self.into_key_code())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAccelerator for &str {
|
||||
fn accelerator(&self) -> Accelerator {
|
||||
Accelerator::from_str(self).unwrap()
|
||||
fn accelerator(&self) -> HotKey {
|
||||
HotKey::from_str(self).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -220,143 +210,144 @@ impl Drop for ShortcutHandle {
|
|||
}
|
||||
|
||||
pub trait IntoModifersState {
|
||||
fn into_modifiers_state(self) -> ModifiersState;
|
||||
fn into_modifiers_state(self) -> Modifiers;
|
||||
}
|
||||
|
||||
impl IntoModifersState for ModifiersState {
|
||||
fn into_modifiers_state(self) -> ModifiersState {
|
||||
self
|
||||
fn into_modifiers_state(self) -> Modifiers {
|
||||
let mut modifiers = Modifiers::default();
|
||||
if self.shift_key() {
|
||||
modifiers |= Modifiers::SHIFT;
|
||||
}
|
||||
if self.control_key() {
|
||||
modifiers |= Modifiers::CONTROL;
|
||||
}
|
||||
if self.alt_key() {
|
||||
modifiers |= Modifiers::ALT;
|
||||
}
|
||||
if self.super_key() {
|
||||
modifiers |= Modifiers::META;
|
||||
}
|
||||
|
||||
modifiers
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoModifersState for Modifiers {
|
||||
fn into_modifiers_state(self) -> ModifiersState {
|
||||
let mut state = ModifiersState::empty();
|
||||
if self.contains(Modifiers::SHIFT) {
|
||||
state |= ModifiersState::SHIFT
|
||||
}
|
||||
if self.contains(Modifiers::CONTROL) {
|
||||
state |= ModifiersState::CONTROL
|
||||
}
|
||||
if self.contains(Modifiers::ALT) {
|
||||
state |= ModifiersState::ALT
|
||||
}
|
||||
if self.contains(Modifiers::META) || self.contains(Modifiers::SUPER) {
|
||||
state |= ModifiersState::SUPER
|
||||
}
|
||||
state
|
||||
fn into_modifiers_state(self) -> Modifiers {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoKeyCode {
|
||||
fn into_key_code(self) -> KeyCode;
|
||||
fn into_key_code(self) -> Code;
|
||||
}
|
||||
|
||||
impl IntoKeyCode for KeyCode {
|
||||
fn into_key_code(self) -> KeyCode {
|
||||
impl IntoKeyCode for Code {
|
||||
fn into_key_code(self) -> Code {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoKeyCode for dioxus_html::KeyCode {
|
||||
fn into_key_code(self) -> KeyCode {
|
||||
fn into_key_code(self) -> Code {
|
||||
match self {
|
||||
dioxus_html::KeyCode::Backspace => KeyCode::Backspace,
|
||||
dioxus_html::KeyCode::Tab => KeyCode::Tab,
|
||||
dioxus_html::KeyCode::Clear => KeyCode::NumpadClear,
|
||||
dioxus_html::KeyCode::Enter => KeyCode::Enter,
|
||||
dioxus_html::KeyCode::Shift => KeyCode::ShiftLeft,
|
||||
dioxus_html::KeyCode::Ctrl => KeyCode::ControlLeft,
|
||||
dioxus_html::KeyCode::Alt => KeyCode::AltLeft,
|
||||
dioxus_html::KeyCode::Pause => KeyCode::Pause,
|
||||
dioxus_html::KeyCode::CapsLock => KeyCode::CapsLock,
|
||||
dioxus_html::KeyCode::Escape => KeyCode::Escape,
|
||||
dioxus_html::KeyCode::Space => KeyCode::Space,
|
||||
dioxus_html::KeyCode::PageUp => KeyCode::PageUp,
|
||||
dioxus_html::KeyCode::PageDown => KeyCode::PageDown,
|
||||
dioxus_html::KeyCode::End => KeyCode::End,
|
||||
dioxus_html::KeyCode::Home => KeyCode::Home,
|
||||
dioxus_html::KeyCode::LeftArrow => KeyCode::ArrowLeft,
|
||||
dioxus_html::KeyCode::UpArrow => KeyCode::ArrowUp,
|
||||
dioxus_html::KeyCode::RightArrow => KeyCode::ArrowRight,
|
||||
dioxus_html::KeyCode::DownArrow => KeyCode::ArrowDown,
|
||||
dioxus_html::KeyCode::Insert => KeyCode::Insert,
|
||||
dioxus_html::KeyCode::Delete => KeyCode::Delete,
|
||||
dioxus_html::KeyCode::Num0 => KeyCode::Numpad0,
|
||||
dioxus_html::KeyCode::Num1 => KeyCode::Numpad1,
|
||||
dioxus_html::KeyCode::Num2 => KeyCode::Numpad2,
|
||||
dioxus_html::KeyCode::Num3 => KeyCode::Numpad3,
|
||||
dioxus_html::KeyCode::Num4 => KeyCode::Numpad4,
|
||||
dioxus_html::KeyCode::Num5 => KeyCode::Numpad5,
|
||||
dioxus_html::KeyCode::Num6 => KeyCode::Numpad6,
|
||||
dioxus_html::KeyCode::Num7 => KeyCode::Numpad7,
|
||||
dioxus_html::KeyCode::Num8 => KeyCode::Numpad8,
|
||||
dioxus_html::KeyCode::Num9 => KeyCode::Numpad9,
|
||||
dioxus_html::KeyCode::A => KeyCode::KeyA,
|
||||
dioxus_html::KeyCode::B => KeyCode::KeyB,
|
||||
dioxus_html::KeyCode::C => KeyCode::KeyC,
|
||||
dioxus_html::KeyCode::D => KeyCode::KeyD,
|
||||
dioxus_html::KeyCode::E => KeyCode::KeyE,
|
||||
dioxus_html::KeyCode::F => KeyCode::KeyF,
|
||||
dioxus_html::KeyCode::G => KeyCode::KeyG,
|
||||
dioxus_html::KeyCode::H => KeyCode::KeyH,
|
||||
dioxus_html::KeyCode::I => KeyCode::KeyI,
|
||||
dioxus_html::KeyCode::J => KeyCode::KeyJ,
|
||||
dioxus_html::KeyCode::K => KeyCode::KeyK,
|
||||
dioxus_html::KeyCode::L => KeyCode::KeyL,
|
||||
dioxus_html::KeyCode::M => KeyCode::KeyM,
|
||||
dioxus_html::KeyCode::N => KeyCode::KeyN,
|
||||
dioxus_html::KeyCode::O => KeyCode::KeyO,
|
||||
dioxus_html::KeyCode::P => KeyCode::KeyP,
|
||||
dioxus_html::KeyCode::Q => KeyCode::KeyQ,
|
||||
dioxus_html::KeyCode::R => KeyCode::KeyR,
|
||||
dioxus_html::KeyCode::S => KeyCode::KeyS,
|
||||
dioxus_html::KeyCode::T => KeyCode::KeyT,
|
||||
dioxus_html::KeyCode::U => KeyCode::KeyU,
|
||||
dioxus_html::KeyCode::V => KeyCode::KeyV,
|
||||
dioxus_html::KeyCode::W => KeyCode::KeyW,
|
||||
dioxus_html::KeyCode::X => KeyCode::KeyX,
|
||||
dioxus_html::KeyCode::Y => KeyCode::KeyY,
|
||||
dioxus_html::KeyCode::Z => KeyCode::KeyZ,
|
||||
dioxus_html::KeyCode::Numpad0 => KeyCode::Numpad0,
|
||||
dioxus_html::KeyCode::Numpad1 => KeyCode::Numpad1,
|
||||
dioxus_html::KeyCode::Numpad2 => KeyCode::Numpad2,
|
||||
dioxus_html::KeyCode::Numpad3 => KeyCode::Numpad3,
|
||||
dioxus_html::KeyCode::Numpad4 => KeyCode::Numpad4,
|
||||
dioxus_html::KeyCode::Numpad5 => KeyCode::Numpad5,
|
||||
dioxus_html::KeyCode::Numpad6 => KeyCode::Numpad6,
|
||||
dioxus_html::KeyCode::Numpad7 => KeyCode::Numpad7,
|
||||
dioxus_html::KeyCode::Numpad8 => KeyCode::Numpad8,
|
||||
dioxus_html::KeyCode::Numpad9 => KeyCode::Numpad9,
|
||||
dioxus_html::KeyCode::Multiply => KeyCode::NumpadMultiply,
|
||||
dioxus_html::KeyCode::Add => KeyCode::NumpadAdd,
|
||||
dioxus_html::KeyCode::Subtract => KeyCode::NumpadSubtract,
|
||||
dioxus_html::KeyCode::DecimalPoint => KeyCode::NumpadDecimal,
|
||||
dioxus_html::KeyCode::Divide => KeyCode::NumpadDivide,
|
||||
dioxus_html::KeyCode::F1 => KeyCode::F1,
|
||||
dioxus_html::KeyCode::F2 => KeyCode::F2,
|
||||
dioxus_html::KeyCode::F3 => KeyCode::F3,
|
||||
dioxus_html::KeyCode::F4 => KeyCode::F4,
|
||||
dioxus_html::KeyCode::F5 => KeyCode::F5,
|
||||
dioxus_html::KeyCode::F6 => KeyCode::F6,
|
||||
dioxus_html::KeyCode::F7 => KeyCode::F7,
|
||||
dioxus_html::KeyCode::F8 => KeyCode::F8,
|
||||
dioxus_html::KeyCode::F9 => KeyCode::F9,
|
||||
dioxus_html::KeyCode::F10 => KeyCode::F10,
|
||||
dioxus_html::KeyCode::F11 => KeyCode::F11,
|
||||
dioxus_html::KeyCode::F12 => KeyCode::F12,
|
||||
dioxus_html::KeyCode::NumLock => KeyCode::NumLock,
|
||||
dioxus_html::KeyCode::ScrollLock => KeyCode::ScrollLock,
|
||||
dioxus_html::KeyCode::Semicolon => KeyCode::Semicolon,
|
||||
dioxus_html::KeyCode::EqualSign => KeyCode::Equal,
|
||||
dioxus_html::KeyCode::Comma => KeyCode::Comma,
|
||||
dioxus_html::KeyCode::Period => KeyCode::Period,
|
||||
dioxus_html::KeyCode::ForwardSlash => KeyCode::Slash,
|
||||
dioxus_html::KeyCode::GraveAccent => KeyCode::Backquote,
|
||||
dioxus_html::KeyCode::OpenBracket => KeyCode::BracketLeft,
|
||||
dioxus_html::KeyCode::BackSlash => KeyCode::Backslash,
|
||||
dioxus_html::KeyCode::CloseBraket => KeyCode::BracketRight,
|
||||
dioxus_html::KeyCode::SingleQuote => KeyCode::Quote,
|
||||
dioxus_html::KeyCode::Backspace => Code::Backspace,
|
||||
dioxus_html::KeyCode::Tab => Code::Tab,
|
||||
dioxus_html::KeyCode::Clear => Code::NumpadClear,
|
||||
dioxus_html::KeyCode::Enter => Code::Enter,
|
||||
dioxus_html::KeyCode::Shift => Code::ShiftLeft,
|
||||
dioxus_html::KeyCode::Ctrl => Code::ControlLeft,
|
||||
dioxus_html::KeyCode::Alt => Code::AltLeft,
|
||||
dioxus_html::KeyCode::Pause => Code::Pause,
|
||||
dioxus_html::KeyCode::CapsLock => Code::CapsLock,
|
||||
dioxus_html::KeyCode::Escape => Code::Escape,
|
||||
dioxus_html::KeyCode::Space => Code::Space,
|
||||
dioxus_html::KeyCode::PageUp => Code::PageUp,
|
||||
dioxus_html::KeyCode::PageDown => Code::PageDown,
|
||||
dioxus_html::KeyCode::End => Code::End,
|
||||
dioxus_html::KeyCode::Home => Code::Home,
|
||||
dioxus_html::KeyCode::LeftArrow => Code::ArrowLeft,
|
||||
dioxus_html::KeyCode::UpArrow => Code::ArrowUp,
|
||||
dioxus_html::KeyCode::RightArrow => Code::ArrowRight,
|
||||
dioxus_html::KeyCode::DownArrow => Code::ArrowDown,
|
||||
dioxus_html::KeyCode::Insert => Code::Insert,
|
||||
dioxus_html::KeyCode::Delete => Code::Delete,
|
||||
dioxus_html::KeyCode::Num0 => Code::Numpad0,
|
||||
dioxus_html::KeyCode::Num1 => Code::Numpad1,
|
||||
dioxus_html::KeyCode::Num2 => Code::Numpad2,
|
||||
dioxus_html::KeyCode::Num3 => Code::Numpad3,
|
||||
dioxus_html::KeyCode::Num4 => Code::Numpad4,
|
||||
dioxus_html::KeyCode::Num5 => Code::Numpad5,
|
||||
dioxus_html::KeyCode::Num6 => Code::Numpad6,
|
||||
dioxus_html::KeyCode::Num7 => Code::Numpad7,
|
||||
dioxus_html::KeyCode::Num8 => Code::Numpad8,
|
||||
dioxus_html::KeyCode::Num9 => Code::Numpad9,
|
||||
dioxus_html::KeyCode::A => Code::KeyA,
|
||||
dioxus_html::KeyCode::B => Code::KeyB,
|
||||
dioxus_html::KeyCode::C => Code::KeyC,
|
||||
dioxus_html::KeyCode::D => Code::KeyD,
|
||||
dioxus_html::KeyCode::E => Code::KeyE,
|
||||
dioxus_html::KeyCode::F => Code::KeyF,
|
||||
dioxus_html::KeyCode::G => Code::KeyG,
|
||||
dioxus_html::KeyCode::H => Code::KeyH,
|
||||
dioxus_html::KeyCode::I => Code::KeyI,
|
||||
dioxus_html::KeyCode::J => Code::KeyJ,
|
||||
dioxus_html::KeyCode::K => Code::KeyK,
|
||||
dioxus_html::KeyCode::L => Code::KeyL,
|
||||
dioxus_html::KeyCode::M => Code::KeyM,
|
||||
dioxus_html::KeyCode::N => Code::KeyN,
|
||||
dioxus_html::KeyCode::O => Code::KeyO,
|
||||
dioxus_html::KeyCode::P => Code::KeyP,
|
||||
dioxus_html::KeyCode::Q => Code::KeyQ,
|
||||
dioxus_html::KeyCode::R => Code::KeyR,
|
||||
dioxus_html::KeyCode::S => Code::KeyS,
|
||||
dioxus_html::KeyCode::T => Code::KeyT,
|
||||
dioxus_html::KeyCode::U => Code::KeyU,
|
||||
dioxus_html::KeyCode::V => Code::KeyV,
|
||||
dioxus_html::KeyCode::W => Code::KeyW,
|
||||
dioxus_html::KeyCode::X => Code::KeyX,
|
||||
dioxus_html::KeyCode::Y => Code::KeyY,
|
||||
dioxus_html::KeyCode::Z => Code::KeyZ,
|
||||
dioxus_html::KeyCode::Numpad0 => Code::Numpad0,
|
||||
dioxus_html::KeyCode::Numpad1 => Code::Numpad1,
|
||||
dioxus_html::KeyCode::Numpad2 => Code::Numpad2,
|
||||
dioxus_html::KeyCode::Numpad3 => Code::Numpad3,
|
||||
dioxus_html::KeyCode::Numpad4 => Code::Numpad4,
|
||||
dioxus_html::KeyCode::Numpad5 => Code::Numpad5,
|
||||
dioxus_html::KeyCode::Numpad6 => Code::Numpad6,
|
||||
dioxus_html::KeyCode::Numpad7 => Code::Numpad7,
|
||||
dioxus_html::KeyCode::Numpad8 => Code::Numpad8,
|
||||
dioxus_html::KeyCode::Numpad9 => Code::Numpad9,
|
||||
dioxus_html::KeyCode::Multiply => Code::NumpadMultiply,
|
||||
dioxus_html::KeyCode::Add => Code::NumpadAdd,
|
||||
dioxus_html::KeyCode::Subtract => Code::NumpadSubtract,
|
||||
dioxus_html::KeyCode::DecimalPoint => Code::NumpadDecimal,
|
||||
dioxus_html::KeyCode::Divide => Code::NumpadDivide,
|
||||
dioxus_html::KeyCode::F1 => Code::F1,
|
||||
dioxus_html::KeyCode::F2 => Code::F2,
|
||||
dioxus_html::KeyCode::F3 => Code::F3,
|
||||
dioxus_html::KeyCode::F4 => Code::F4,
|
||||
dioxus_html::KeyCode::F5 => Code::F5,
|
||||
dioxus_html::KeyCode::F6 => Code::F6,
|
||||
dioxus_html::KeyCode::F7 => Code::F7,
|
||||
dioxus_html::KeyCode::F8 => Code::F8,
|
||||
dioxus_html::KeyCode::F9 => Code::F9,
|
||||
dioxus_html::KeyCode::F10 => Code::F10,
|
||||
dioxus_html::KeyCode::F11 => Code::F11,
|
||||
dioxus_html::KeyCode::F12 => Code::F12,
|
||||
dioxus_html::KeyCode::NumLock => Code::NumLock,
|
||||
dioxus_html::KeyCode::ScrollLock => Code::ScrollLock,
|
||||
dioxus_html::KeyCode::Semicolon => Code::Semicolon,
|
||||
dioxus_html::KeyCode::EqualSign => Code::Equal,
|
||||
dioxus_html::KeyCode::Comma => Code::Comma,
|
||||
dioxus_html::KeyCode::Period => Code::Period,
|
||||
dioxus_html::KeyCode::ForwardSlash => Code::Slash,
|
||||
dioxus_html::KeyCode::GraveAccent => Code::Backquote,
|
||||
dioxus_html::KeyCode::OpenBracket => Code::BracketLeft,
|
||||
dioxus_html::KeyCode::BackSlash => Code::Backslash,
|
||||
dioxus_html::KeyCode::CloseBraket => Code::BracketRight,
|
||||
dioxus_html::KeyCode::SingleQuote => Code::Quote,
|
||||
key => panic!("Failed to convert {:?} to tao::keyboard::KeyCode, try using tao::keyboard::KeyCode directly", key),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
use crate::desktop_context::EventData;
|
||||
use crate::protocol;
|
||||
use crate::protocol::{self, AssetHandlerRegistry};
|
||||
use crate::{desktop_context::UserWindowEvent, Config};
|
||||
use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
|
||||
pub use wry;
|
||||
pub use wry::application as tao;
|
||||
use wry::application::menu::{MenuBar, MenuItem};
|
||||
use wry::application::window::Window;
|
||||
use wry::http::Response;
|
||||
use wry::webview::{WebContext, WebView, WebViewBuilder};
|
||||
|
||||
pub fn build(
|
||||
cfg: &mut Config,
|
||||
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
|
||||
proxy: EventLoopProxy<UserWindowEvent>,
|
||||
) -> (WebView, WebContext) {
|
||||
let mut builder = cfg.window.clone();
|
||||
) -> (WebView, WebContext, AssetHandlerRegistry) {
|
||||
let window = builder.with_visible(false).build(event_loop).unwrap();
|
||||
let file_handler = cfg.file_drop_handler.take();
|
||||
let custom_head = cfg.custom_head.clone();
|
||||
let index_file = cfg.custom_index.clone();
|
||||
|
@ -38,6 +39,8 @@ pub fn build(
|
|||
}
|
||||
|
||||
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)
|
||||
.unwrap()
|
||||
|
@ -50,8 +53,29 @@ pub fn build(
|
|||
_ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window.id()));
|
||||
}
|
||||
})
|
||||
.with_custom_protocol(String::from("dioxus"), move |r| {
|
||||
protocol::desktop_handler(r, custom_head.clone(), index_file.clone(), &root_name)
|
||||
.with_asynchronous_custom_protocol(String::from("dioxus"), move |request, responder| {
|
||||
let custom_head = custom_head.clone();
|
||||
let index_file = index_file.clone();
|
||||
let root_name = root_name.clone();
|
||||
let asset_handlers_ref = asset_handlers_ref.clone();
|
||||
tokio::spawn(async move {
|
||||
let response_res = protocol::desktop_handler(
|
||||
request,
|
||||
custom_head.clone(),
|
||||
index_file.clone(),
|
||||
&root_name,
|
||||
&asset_handlers_ref,
|
||||
)
|
||||
.await;
|
||||
let response = response_res.unwrap_or_else(|err| {
|
||||
tracing::error!("Error: {}", err);
|
||||
Response::builder()
|
||||
.status(500)
|
||||
.body(err.to_string().into_bytes().into())
|
||||
.unwrap()
|
||||
});
|
||||
responder.respond(response);
|
||||
});
|
||||
})
|
||||
.with_file_drop_handler(move |window, evet| {
|
||||
file_handler
|
||||
|
@ -77,7 +101,16 @@ pub fn build(
|
|||
// .with_web_context(&mut web_context);
|
||||
|
||||
for (name, handler) in cfg.protocols.drain(..) {
|
||||
webview = webview.with_custom_protocol(name, handler)
|
||||
webview = webview.with_custom_protocol(name, move |r| match handler(&r) {
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
tracing::error!("Error: {}", err);
|
||||
Response::builder()
|
||||
.status(500)
|
||||
.body(err.to_string().into_bytes().into())
|
||||
.unwrap()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if cfg.disable_context_menu {
|
||||
|
@ -100,7 +133,7 @@ pub fn build(
|
|||
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
|
||||
|
|
|
@ -37,7 +37,7 @@ fn app(cx: Scope) -> Element {
|
|||
// todo: remove
|
||||
let mut trimmed = format!("{event:?}");
|
||||
trimmed.truncate(200);
|
||||
rsx!(p { "{trimmed}" })
|
||||
rsx!( p { "{trimmed}" } )
|
||||
});
|
||||
|
||||
let log_event = move |event: Event| {
|
||||
|
@ -45,10 +45,7 @@ fn app(cx: Scope) -> Element {
|
|||
};
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
flex_direction: "column",
|
||||
div { width: "100%", height: "100%", flex_direction: "column",
|
||||
div {
|
||||
width: "80%",
|
||||
height: "50%",
|
||||
|
@ -59,7 +56,7 @@ fn app(cx: Scope) -> Element {
|
|||
|
||||
onmousemove: move |event| log_event(Event::MouseMove(event.inner().clone())),
|
||||
onclick: move |event| log_event(Event::MouseClick(event.inner().clone())),
|
||||
ondblclick: move |event| log_event(Event::MouseDoubleClick(event.inner().clone())),
|
||||
ondoubleclick: move |event| log_event(Event::MouseDoubleClick(event.inner().clone())),
|
||||
onmousedown: move |event| log_event(Event::MouseDown(event.inner().clone())),
|
||||
onmouseup: move |event| log_event(Event::MouseUp(event.inner().clone())),
|
||||
|
||||
|
@ -73,13 +70,8 @@ fn app(cx: Scope) -> Element {
|
|||
onfocusout: move |event| log_event(Event::FocusOut(event.inner().clone())),
|
||||
|
||||
"Hover, click, type or scroll to see the info down below"
|
||||
},
|
||||
div {
|
||||
width: "80%",
|
||||
height: "50%",
|
||||
flex_direction: "column",
|
||||
events_rendered,
|
||||
},
|
||||
},
|
||||
}
|
||||
div { width: "80%", height: "50%", flex_direction: "column", events_rendered }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -15,21 +15,21 @@ fn app(cx: Scope) -> Element {
|
|||
let mapping: DioxusElementToNodeId = cx.consume_context().unwrap();
|
||||
// disable templates so that every node has an id and can be queried
|
||||
cx.render(rsx! {
|
||||
div{
|
||||
div {
|
||||
width: "100%",
|
||||
background_color: "hsl({hue}, 70%, {brightness}%)",
|
||||
onmousemove: move |evt| {
|
||||
if let RenderReturn::Ready(node) = cx.root_node() {
|
||||
if let Some(id) = node.root_ids.borrow().get(0).cloned() {
|
||||
if let Some(id) = node.root_ids.borrow().first().cloned() {
|
||||
let node = tui_query.get(mapping.get_node_id(id).unwrap());
|
||||
let Size{width, height} = node.size().unwrap();
|
||||
let Size { width, height } = node.size().unwrap();
|
||||
let pos = evt.inner().element_coordinates();
|
||||
hue.set((pos.x as f32/width as f32)*255.0);
|
||||
brightness.set((pos.y as f32/height as f32)*100.0);
|
||||
hue.set((pos.x as f32 / width as f32) * 255.0);
|
||||
brightness.set((pos.y as f32 / height as f32) * 100.0);
|
||||
}
|
||||
}
|
||||
},
|
||||
"hsl({hue}, 70%, {brightness}%)",
|
||||
"hsl({hue}, 70%, {brightness}%)"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,17 +1,39 @@
|
|||
//! This file exports functions into the vscode extension
|
||||
|
||||
use dioxus_autofmt::FormattedBlock;
|
||||
use dioxus_autofmt::{FormattedBlock, IndentOptions, IndentType};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn format_rsx(raw: String) -> String {
|
||||
let block = dioxus_autofmt::fmt_block(&raw, 0);
|
||||
pub fn format_rsx(raw: String, use_tabs: bool, indent_size: usize) -> String {
|
||||
let block = dioxus_autofmt::fmt_block(
|
||||
&raw,
|
||||
0,
|
||||
IndentOptions::new(
|
||||
if use_tabs {
|
||||
IndentType::Tabs
|
||||
} else {
|
||||
IndentType::Spaces
|
||||
},
|
||||
indent_size,
|
||||
),
|
||||
);
|
||||
block.unwrap()
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn format_selection(raw: String) -> String {
|
||||
let block = dioxus_autofmt::fmt_block(&raw, 0);
|
||||
pub fn format_selection(raw: String, use_tabs: bool, indent_size: usize) -> String {
|
||||
let block = dioxus_autofmt::fmt_block(
|
||||
&raw,
|
||||
0,
|
||||
IndentOptions::new(
|
||||
if use_tabs {
|
||||
IndentType::Tabs
|
||||
} else {
|
||||
IndentType::Spaces
|
||||
},
|
||||
indent_size,
|
||||
),
|
||||
);
|
||||
block.unwrap()
|
||||
}
|
||||
|
||||
|
@ -35,8 +57,18 @@ impl FormatBlockInstance {
|
|||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn format_file(contents: String) -> FormatBlockInstance {
|
||||
let _edits = dioxus_autofmt::fmt_file(&contents);
|
||||
pub fn format_file(contents: String, use_tabs: bool, indent_size: usize) -> FormatBlockInstance {
|
||||
let _edits = dioxus_autofmt::fmt_file(
|
||||
&contents,
|
||||
IndentOptions::new(
|
||||
if use_tabs {
|
||||
IndentType::Tabs
|
||||
} else {
|
||||
IndentType::Spaces
|
||||
},
|
||||
indent_size,
|
||||
),
|
||||
);
|
||||
let out = dioxus_autofmt::apply_formats(&contents, _edits.clone());
|
||||
FormatBlockInstance { new: out, _edits }
|
||||
}
|
||||
|
|
|
@ -90,7 +90,13 @@ function fmtDocument(document: vscode.TextDocument) {
|
|||
if (!editor) return; // Need an editor to apply text edits.
|
||||
|
||||
const contents = editor.document.getText();
|
||||
const formatted = dioxus.format_file(contents);
|
||||
let tabSize: number;
|
||||
if (typeof editor.options.tabSize === 'number') {
|
||||
tabSize = editor.options.tabSize;
|
||||
} else {
|
||||
tabSize = 4;
|
||||
}
|
||||
const formatted = dioxus.format_file(contents, !editor.options.insertSpaces, tabSize);
|
||||
|
||||
// Replace the entire text document
|
||||
// Yes, this is a bit heavy handed, but the dioxus side doesn't know the line/col scheme that vscode is using
|
||||
|
|
|
@ -22,8 +22,6 @@ mod atoms {
|
|||
pub use atom::*;
|
||||
pub use atomfamily::*;
|
||||
pub use atomref::*;
|
||||
pub use selector::*;
|
||||
pub use selectorfamily::*;
|
||||
}
|
||||
|
||||
pub mod hooks {
|
||||
|
|
|
@ -26,7 +26,7 @@ tower = { version = "0.4.13", features = ["util"], optional = true }
|
|||
axum-macros = "0.3.7"
|
||||
|
||||
# salvo
|
||||
salvo = { version = "0.46.0", optional = true, features = ["serve-static", "websocket", "compression"] }
|
||||
salvo = { version = "0.63.0", optional = true, features = ["serve-static", "websocket", "compression"] }
|
||||
serde = "1.0.159"
|
||||
|
||||
# Dioxus + SSR
|
||||
|
|
|
@ -24,6 +24,7 @@ fn app(cx: Scope<AppProps>) -> Element {
|
|||
|
||||
let mut count = use_state(cx, || 0);
|
||||
let text = use_state(cx, || "...".to_string());
|
||||
let eval = use_eval(cx);
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
|
|
|
@ -12,7 +12,7 @@ dioxus = { workspace = true }
|
|||
dioxus-fullstack = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"], optional = true }
|
||||
serde = "1.0.159"
|
||||
salvo = { version = "0.37.9", optional = true }
|
||||
salvo = { version = "0.63.0", optional = true }
|
||||
execute = "0.2.12"
|
||||
reqwest = "0.11.18"
|
||||
simple_logger = "4.2.0"
|
||||
|
|
|
@ -89,26 +89,26 @@ impl Service for ServerFnHandler {
|
|||
let parts = Arc::new(RwLock::new(parts));
|
||||
|
||||
// Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime
|
||||
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
|
||||
let pool = get_local_pool();
|
||||
pool.spawn_pinned({
|
||||
let function = function.clone();
|
||||
let mut server_context = server_context.clone();
|
||||
server_context.parts = parts;
|
||||
move || async move {
|
||||
let data = match function.encoding() {
|
||||
Encoding::Url | Encoding::Cbor => &body,
|
||||
Encoding::GetJSON | Encoding::GetCBOR => &query,
|
||||
};
|
||||
let server_function_future = function.call((), data);
|
||||
let server_function_future =
|
||||
ProvideServerContext::new(server_function_future, server_context.clone());
|
||||
let resp = server_function_future.await;
|
||||
|
||||
resp_tx.send(resp).unwrap();
|
||||
}
|
||||
});
|
||||
let result = resp_rx.await.unwrap();
|
||||
let result = pool
|
||||
.spawn_pinned({
|
||||
let function = function.clone();
|
||||
let mut server_context = server_context.clone();
|
||||
server_context.parts = parts;
|
||||
move || async move {
|
||||
let data = match function.encoding() {
|
||||
Encoding::Url | Encoding::Cbor => &body,
|
||||
Encoding::GetJSON | Encoding::GetCBOR => &query,
|
||||
};
|
||||
let server_function_future = function.call((), data);
|
||||
let server_function_future = ProvideServerContext::new(
|
||||
server_function_future,
|
||||
server_context.clone(),
|
||||
);
|
||||
server_function_future.await
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
let mut res = http::Response::builder();
|
||||
|
||||
// Set the headers from the server context
|
||||
|
|
|
@ -12,9 +12,11 @@ use serde::{de::DeserializeOwned, Serialize};
|
|||
/// use dioxus_fullstack::prelude::*;
|
||||
///
|
||||
/// fn app(cx: Scope) -> Element {
|
||||
/// let state1 = use_state(cx, || from_server(|| {
|
||||
/// let state1 = use_state(cx, || server_cached(|| {
|
||||
/// 1234
|
||||
/// }));
|
||||
///
|
||||
/// todo!()
|
||||
/// }
|
||||
/// ```
|
||||
pub fn server_cached<O: 'static + Serialize + DeserializeOwned>(server_fn: impl Fn() -> O) -> O {
|
||||
|
|
|
@ -3,7 +3,9 @@ use tracing_futures::Instrument;
|
|||
|
||||
use http::{Request, Response};
|
||||
|
||||
/// A layer that wraps a service. This can be used to add additional information to the request, or response on top of some other service
|
||||
pub trait Layer: Send + Sync + 'static {
|
||||
/// Wrap a boxed service with this layer
|
||||
fn layer(&self, inner: BoxedService) -> BoxedService;
|
||||
}
|
||||
|
||||
|
@ -17,7 +19,9 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// A service is a function that takes a request and returns an async response
|
||||
pub trait Service {
|
||||
/// Run the service and produce a future that resolves to a response
|
||||
fn run(
|
||||
&mut self,
|
||||
req: http::Request<hyper::body::Body>,
|
||||
|
@ -55,6 +59,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// A boxed service is a type-erased service that can be used without knowing the underlying type
|
||||
pub struct BoxedService(pub Box<dyn Service + Send>);
|
||||
|
||||
impl tower::Service<http::Request<hyper::body::Body>> for BoxedService {
|
||||
|
|
|
@ -40,6 +40,8 @@ pub mod prelude {
|
|||
#[cfg(not(feature = "ssr"))]
|
||||
pub use crate::html_storage::deserialize::get_root_props_from_document;
|
||||
pub use crate::launch::LaunchBuilder;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub use crate::layer::{Layer, Service};
|
||||
#[cfg(all(feature = "ssr", feature = "router"))]
|
||||
pub use crate::render::pre_cache_static_routes_with_props;
|
||||
#[cfg(feature = "ssr")]
|
||||
|
|
|
@ -45,6 +45,8 @@ impl SsrRendererPool {
|
|||
.expect("couldn't spawn runtime")
|
||||
.block_on(async move {
|
||||
let mut vdom = VirtualDom::new_with_props(component, props);
|
||||
// Make sure the evaluator is initialized
|
||||
dioxus_ssr::eval::init_eval(vdom.base_scope());
|
||||
let mut to = WriteBuffer { buffer: Vec::new() };
|
||||
// before polling the future, we need to set the context
|
||||
let prev_context =
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "generational-box"
|
||||
authors = ["Evan Almloff"]
|
||||
version = "0.1.0"
|
||||
version = "0.4.3"
|
||||
edition = "2018"
|
||||
description = "A box backed by a generational runtime"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
@ -18,3 +18,5 @@ rand = "0.8.5"
|
|||
[features]
|
||||
default = ["check_generation"]
|
||||
check_generation = []
|
||||
debug_borrows = []
|
||||
debug_ownership = []
|
||||
|
|
|
@ -11,6 +11,8 @@ Three main types manage state in Generational Box:
|
|||
Example:
|
||||
|
||||
```rust
|
||||
use generational_box::Store;
|
||||
|
||||
// Create a store for this thread
|
||||
let store = Store::default();
|
||||
|
||||
|
|
|
@ -2,9 +2,12 @@
|
|||
#![warn(missing_docs)]
|
||||
|
||||
use std::{
|
||||
any::Any,
|
||||
cell::{Cell, Ref, RefCell, RefMut},
|
||||
fmt::Debug,
|
||||
error::Error,
|
||||
fmt::{Debug, Display},
|
||||
marker::PhantomData,
|
||||
ops::{Deref, DerefMut},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
|
@ -29,12 +32,12 @@ fn reused() {
|
|||
let first_ptr;
|
||||
{
|
||||
let owner = store.owner();
|
||||
first_ptr = owner.insert(1).raw.data.as_ptr();
|
||||
first_ptr = owner.insert(1).raw.0.data.as_ptr();
|
||||
drop(owner);
|
||||
}
|
||||
{
|
||||
let owner = store.owner();
|
||||
let second_ptr = owner.insert(1234).raw.data.as_ptr();
|
||||
let second_ptr = owner.insert(1234).raw.0.data.as_ptr();
|
||||
assert_eq!(first_ptr, second_ptr);
|
||||
drop(owner);
|
||||
}
|
||||
|
@ -53,7 +56,10 @@ fn leaking_is_ok() {
|
|||
// don't drop the owner
|
||||
std::mem::forget(owner);
|
||||
}
|
||||
assert_eq!(key.try_read().as_deref(), Some(&"hello world".to_string()));
|
||||
assert_eq!(
|
||||
key.try_read().as_deref().unwrap(),
|
||||
&"hello world".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -68,7 +74,7 @@ fn drops() {
|
|||
key = owner.insert(data);
|
||||
// drop the owner
|
||||
}
|
||||
assert!(key.try_read().is_none());
|
||||
assert!(key.try_read().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -129,7 +135,7 @@ fn fuzz() {
|
|||
println!("{:?}", path);
|
||||
for key in valid_keys.iter() {
|
||||
let value = key.read();
|
||||
println!("{:?}", value);
|
||||
println!("{:?}", &*value);
|
||||
assert!(value.starts_with("hello world"));
|
||||
}
|
||||
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||
|
@ -153,6 +159,8 @@ pub struct GenerationalBox<T> {
|
|||
raw: MemoryLocation,
|
||||
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||
generation: u32,
|
||||
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
|
||||
created_at: &'static std::panic::Location<'static>,
|
||||
_marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
|
@ -161,7 +169,7 @@ impl<T: 'static> Debug for GenerationalBox<T> {
|
|||
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||
f.write_fmt(format_args!(
|
||||
"{:?}@{:?}",
|
||||
self.raw.data.as_ptr(),
|
||||
self.raw.0.data.as_ptr(),
|
||||
self.generation
|
||||
))?;
|
||||
#[cfg(not(any(debug_assertions, feature = "check_generation")))]
|
||||
|
@ -175,7 +183,7 @@ impl<T: 'static> GenerationalBox<T> {
|
|||
fn validate(&self) -> bool {
|
||||
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||
{
|
||||
self.raw.generation.get() == self.generation
|
||||
self.raw.0.generation.get() == self.generation
|
||||
}
|
||||
#[cfg(not(any(debug_assertions, feature = "check_generation")))]
|
||||
{
|
||||
|
@ -184,43 +192,51 @@ impl<T: 'static> GenerationalBox<T> {
|
|||
}
|
||||
|
||||
/// Try to read the value. Returns None if the value is no longer valid.
|
||||
pub fn try_read(&self) -> Option<Ref<'static, T>> {
|
||||
self.validate()
|
||||
.then(|| {
|
||||
Ref::filter_map(self.raw.data.borrow(), |any| {
|
||||
any.as_ref()?.downcast_ref::<T>()
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.flatten()
|
||||
#[track_caller]
|
||||
pub fn try_read(&self) -> Result<GenerationalRef<T>, BorrowError> {
|
||||
if !self.validate() {
|
||||
return Err(BorrowError::Dropped(ValueDroppedError {
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
created_at: self.created_at,
|
||||
}));
|
||||
}
|
||||
self.raw.try_borrow(
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
self.created_at,
|
||||
)
|
||||
}
|
||||
|
||||
/// Read the value. Panics if the value is no longer valid.
|
||||
pub fn read(&self) -> Ref<'static, T> {
|
||||
#[track_caller]
|
||||
pub fn read(&self) -> GenerationalRef<T> {
|
||||
self.try_read().unwrap()
|
||||
}
|
||||
|
||||
/// Try to write the value. Returns None if the value is no longer valid.
|
||||
pub fn try_write(&self) -> Option<RefMut<'static, T>> {
|
||||
self.validate()
|
||||
.then(|| {
|
||||
RefMut::filter_map(self.raw.data.borrow_mut(), |any| {
|
||||
any.as_mut()?.downcast_mut::<T>()
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.flatten()
|
||||
#[track_caller]
|
||||
pub fn try_write(&self) -> Result<GenerationalRefMut<T>, BorrowMutError> {
|
||||
if !self.validate() {
|
||||
return Err(BorrowMutError::Dropped(ValueDroppedError {
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
created_at: self.created_at,
|
||||
}));
|
||||
}
|
||||
self.raw.try_borrow_mut(
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
self.created_at,
|
||||
)
|
||||
}
|
||||
|
||||
/// Write the value. Panics if the value is no longer valid.
|
||||
pub fn write(&self) -> RefMut<'static, T> {
|
||||
#[track_caller]
|
||||
pub fn write(&self) -> GenerationalRefMut<T> {
|
||||
self.try_write().unwrap()
|
||||
}
|
||||
|
||||
/// Set the value. Panics if the value is no longer valid.
|
||||
pub fn set(&self, value: T) {
|
||||
self.validate().then(|| {
|
||||
*self.raw.data.borrow_mut() = Some(Box::new(value));
|
||||
*self.raw.0.data.borrow_mut() = Some(Box::new(value));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -228,7 +244,8 @@ impl<T: 'static> GenerationalBox<T> {
|
|||
pub fn ptr_eq(&self, other: &Self) -> bool {
|
||||
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||
{
|
||||
self.raw.data.as_ptr() == other.raw.data.as_ptr() && self.generation == other.generation
|
||||
self.raw.0.data.as_ptr() == other.raw.0.data.as_ptr()
|
||||
&& self.generation == other.generation
|
||||
}
|
||||
#[cfg(not(any(debug_assertions, feature = "check_generation")))]
|
||||
{
|
||||
|
@ -246,26 +263,37 @@ impl<T> Clone for GenerationalBox<T> {
|
|||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct MemoryLocation {
|
||||
data: &'static RefCell<Option<Box<dyn std::any::Any>>>,
|
||||
struct MemoryLocation(&'static MemoryLocationInner);
|
||||
|
||||
struct MemoryLocationInner {
|
||||
data: RefCell<Option<Box<dyn std::any::Any>>>,
|
||||
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||
generation: &'static Cell<u32>,
|
||||
generation: Cell<u32>,
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
borrowed_at: RefCell<Vec<&'static std::panic::Location<'static>>>,
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
borrowed_mut_at: Cell<Option<&'static std::panic::Location<'static>>>,
|
||||
}
|
||||
|
||||
impl MemoryLocation {
|
||||
#[allow(unused)]
|
||||
fn drop(&self) {
|
||||
let old = self.data.borrow_mut().take();
|
||||
let old = self.0.data.borrow_mut().take();
|
||||
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||
if old.is_some() {
|
||||
drop(old);
|
||||
let new_generation = self.generation.get() + 1;
|
||||
self.generation.set(new_generation);
|
||||
let new_generation = self.0.generation.get() + 1;
|
||||
self.0.generation.set(new_generation);
|
||||
}
|
||||
}
|
||||
|
||||
fn replace<T: 'static>(&mut self, value: T) -> GenerationalBox<T> {
|
||||
let mut inner_mut = self.data.borrow_mut();
|
||||
fn replace_with_caller<T: 'static>(
|
||||
&mut self,
|
||||
value: T,
|
||||
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
|
||||
caller: &'static std::panic::Location<'static>,
|
||||
) -> GenerationalBox<T> {
|
||||
let mut inner_mut = self.0.data.borrow_mut();
|
||||
|
||||
let raw = Box::new(value);
|
||||
let old = inner_mut.replace(raw);
|
||||
|
@ -273,10 +301,315 @@ impl MemoryLocation {
|
|||
GenerationalBox {
|
||||
raw: *self,
|
||||
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||
generation: self.generation.get(),
|
||||
generation: self.0.generation.get(),
|
||||
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
|
||||
created_at: caller,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn try_borrow<T: Any>(
|
||||
&self,
|
||||
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
|
||||
created_at: &'static std::panic::Location<'static>,
|
||||
) -> Result<GenerationalRef<T>, BorrowError> {
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
self.0
|
||||
.borrowed_at
|
||||
.borrow_mut()
|
||||
.push(std::panic::Location::caller());
|
||||
match self.0.data.try_borrow() {
|
||||
Ok(borrow) => match Ref::filter_map(borrow, |any| any.as_ref()?.downcast_ref::<T>()) {
|
||||
Ok(reference) => Ok(GenerationalRef {
|
||||
inner: reference,
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
borrow: GenerationalRefBorrowInfo {
|
||||
borrowed_at: std::panic::Location::caller(),
|
||||
borrowed_from: self.0,
|
||||
},
|
||||
}),
|
||||
Err(_) => Err(BorrowError::Dropped(ValueDroppedError {
|
||||
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
|
||||
created_at,
|
||||
})),
|
||||
},
|
||||
Err(_) => Err(BorrowError::AlreadyBorrowedMut(AlreadyBorrowedMutError {
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
borrowed_mut_at: self.0.borrowed_mut_at.get().unwrap(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn try_borrow_mut<T: Any>(
|
||||
&self,
|
||||
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
|
||||
created_at: &'static std::panic::Location<'static>,
|
||||
) -> Result<GenerationalRefMut<T>, BorrowMutError> {
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
{
|
||||
self.0
|
||||
.borrowed_mut_at
|
||||
.set(Some(std::panic::Location::caller()));
|
||||
}
|
||||
match self.0.data.try_borrow_mut() {
|
||||
Ok(borrow_mut) => {
|
||||
match RefMut::filter_map(borrow_mut, |any| any.as_mut()?.downcast_mut::<T>()) {
|
||||
Ok(reference) => Ok(GenerationalRefMut {
|
||||
inner: reference,
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
borrow: GenerationalRefMutBorrowInfo {
|
||||
borrowed_from: self.0,
|
||||
},
|
||||
}),
|
||||
Err(_) => Err(BorrowMutError::Dropped(ValueDroppedError {
|
||||
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
|
||||
created_at,
|
||||
})),
|
||||
}
|
||||
}
|
||||
Err(_) => Err(BorrowMutError::AlreadyBorrowed(AlreadyBorrowedError {
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
borrowed_at: self.0.borrowed_at.borrow().clone(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// An error that can occur when trying to borrow a value.
|
||||
pub enum BorrowError {
|
||||
/// The value was dropped.
|
||||
Dropped(ValueDroppedError),
|
||||
/// The value was already borrowed mutably.
|
||||
AlreadyBorrowedMut(AlreadyBorrowedMutError),
|
||||
}
|
||||
|
||||
impl Display for BorrowError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
BorrowError::Dropped(error) => Display::fmt(error, f),
|
||||
BorrowError::AlreadyBorrowedMut(error) => Display::fmt(error, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for BorrowError {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// An error that can occur when trying to borrow a value mutably.
|
||||
pub enum BorrowMutError {
|
||||
/// The value was dropped.
|
||||
Dropped(ValueDroppedError),
|
||||
/// The value was already borrowed.
|
||||
AlreadyBorrowed(AlreadyBorrowedError),
|
||||
/// The value was already borrowed mutably.
|
||||
AlreadyBorrowedMut(AlreadyBorrowedMutError),
|
||||
}
|
||||
|
||||
impl Display for BorrowMutError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
BorrowMutError::Dropped(error) => Display::fmt(error, f),
|
||||
BorrowMutError::AlreadyBorrowedMut(error) => Display::fmt(error, f),
|
||||
BorrowMutError::AlreadyBorrowed(error) => Display::fmt(error, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for BorrowMutError {}
|
||||
|
||||
/// An error that can occur when trying to use a value that has been dropped.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ValueDroppedError {
|
||||
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
|
||||
created_at: &'static std::panic::Location<'static>,
|
||||
}
|
||||
|
||||
impl Display for ValueDroppedError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("Failed to borrow because the value was dropped.")?;
|
||||
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
|
||||
f.write_fmt(format_args!("created_at: {}", self.created_at))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ValueDroppedError {}
|
||||
|
||||
/// An error that can occur when trying to borrow a value that has already been borrowed mutably.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct AlreadyBorrowedMutError {
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
borrowed_mut_at: &'static std::panic::Location<'static>,
|
||||
}
|
||||
|
||||
impl Display for AlreadyBorrowedMutError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("Failed to borrow because the value was already borrowed mutably.")?;
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
f.write_fmt(format_args!("borrowed_mut_at: {}", self.borrowed_mut_at))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AlreadyBorrowedMutError {}
|
||||
|
||||
/// An error that can occur when trying to borrow a value mutably that has already been borrowed immutably.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AlreadyBorrowedError {
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
borrowed_at: Vec<&'static std::panic::Location<'static>>,
|
||||
}
|
||||
|
||||
impl Display for AlreadyBorrowedError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("Failed to borrow mutably because the value was already borrowed immutably.")?;
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
f.write_str("borrowed_at:")?;
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
for location in self.borrowed_at.iter() {
|
||||
f.write_fmt(format_args!("\t{}", location))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AlreadyBorrowedError {}
|
||||
|
||||
/// A reference to a value in a generational box.
|
||||
pub struct GenerationalRef<T: 'static> {
|
||||
inner: Ref<'static, T>,
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
borrow: GenerationalRefBorrowInfo,
|
||||
}
|
||||
|
||||
impl<T: 'static> GenerationalRef<T> {
|
||||
/// Map one ref type to another.
|
||||
pub fn map<U, F>(orig: GenerationalRef<T>, f: F) -> GenerationalRef<U>
|
||||
where
|
||||
F: FnOnce(&T) -> &U,
|
||||
{
|
||||
GenerationalRef {
|
||||
inner: Ref::map(orig.inner, f),
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
borrow: GenerationalRefBorrowInfo {
|
||||
borrowed_at: orig.borrow.borrowed_at,
|
||||
borrowed_from: orig.borrow.borrowed_from,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter one ref type to another.
|
||||
pub fn filter_map<U, F>(orig: GenerationalRef<T>, f: F) -> Option<GenerationalRef<U>>
|
||||
where
|
||||
F: FnOnce(&T) -> Option<&U>,
|
||||
{
|
||||
let Self {
|
||||
inner,
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
borrow,
|
||||
} = orig;
|
||||
Ref::filter_map(inner, f).ok().map(|inner| GenerationalRef {
|
||||
inner,
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
borrow: GenerationalRefBorrowInfo {
|
||||
borrowed_at: borrow.borrowed_at,
|
||||
borrowed_from: borrow.borrowed_from,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> Deref for GenerationalRef<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.inner.deref()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
struct GenerationalRefBorrowInfo {
|
||||
borrowed_at: &'static std::panic::Location<'static>,
|
||||
borrowed_from: &'static MemoryLocationInner,
|
||||
}
|
||||
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
impl Drop for GenerationalRefBorrowInfo {
|
||||
fn drop(&mut self) {
|
||||
self.borrowed_from
|
||||
.borrowed_at
|
||||
.borrow_mut()
|
||||
.retain(|location| std::ptr::eq(*location, self.borrowed_at as *const _));
|
||||
}
|
||||
}
|
||||
|
||||
/// A mutable reference to a value in a generational box.
|
||||
pub struct GenerationalRefMut<T: 'static> {
|
||||
inner: RefMut<'static, T>,
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
borrow: GenerationalRefMutBorrowInfo,
|
||||
}
|
||||
|
||||
impl<T: 'static> GenerationalRefMut<T> {
|
||||
/// Map one ref type to another.
|
||||
pub fn map<U, F>(orig: GenerationalRefMut<T>, f: F) -> GenerationalRefMut<U>
|
||||
where
|
||||
F: FnOnce(&mut T) -> &mut U,
|
||||
{
|
||||
GenerationalRefMut {
|
||||
inner: RefMut::map(orig.inner, f),
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
borrow: orig.borrow,
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter one ref type to another.
|
||||
pub fn filter_map<U, F>(orig: GenerationalRefMut<T>, f: F) -> Option<GenerationalRefMut<U>>
|
||||
where
|
||||
F: FnOnce(&mut T) -> Option<&mut U>,
|
||||
{
|
||||
let Self {
|
||||
inner,
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
borrow,
|
||||
} = orig;
|
||||
RefMut::filter_map(inner, f)
|
||||
.ok()
|
||||
.map(|inner| GenerationalRefMut {
|
||||
inner,
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
borrow,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> Deref for GenerationalRefMut<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.inner.deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> DerefMut for GenerationalRefMut<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.inner.deref_mut()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
struct GenerationalRefMutBorrowInfo {
|
||||
borrowed_from: &'static MemoryLocationInner,
|
||||
}
|
||||
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
impl Drop for GenerationalRefMutBorrowInfo {
|
||||
fn drop(&mut self) {
|
||||
self.borrowed_from.borrowed_mut_at.take();
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles recycling generational boxes that have been dropped. Your application should have one store or one store per thread.
|
||||
|
@ -305,12 +638,16 @@ impl Store {
|
|||
if let Some(location) = self.recycled.borrow_mut().pop() {
|
||||
location
|
||||
} else {
|
||||
let data: &'static RefCell<_> = self.bump.alloc(RefCell::new(None));
|
||||
MemoryLocation {
|
||||
data,
|
||||
let data: &'static MemoryLocationInner = self.bump.alloc(MemoryLocationInner {
|
||||
data: RefCell::new(None),
|
||||
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||
generation: self.bump.alloc(Cell::new(0)),
|
||||
}
|
||||
generation: Cell::new(0),
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
borrowed_at: Default::default(),
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
borrowed_mut_at: Default::default(),
|
||||
});
|
||||
MemoryLocation(data)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -331,9 +668,31 @@ pub struct Owner {
|
|||
|
||||
impl Owner {
|
||||
/// Insert a value into the store. The value will be dropped when the owner is dropped.
|
||||
#[track_caller]
|
||||
pub fn insert<T: 'static>(&self, value: T) -> GenerationalBox<T> {
|
||||
let mut location = self.store.claim();
|
||||
let key = location.replace(value);
|
||||
let key = location.replace_with_caller(
|
||||
value,
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
std::panic::Location::caller(),
|
||||
);
|
||||
self.owned.borrow_mut().push(location);
|
||||
key
|
||||
}
|
||||
|
||||
/// Insert a value into the store with a specific location blamed for creating the value. The value will be dropped when the owner is dropped.
|
||||
pub fn insert_with_caller<T: 'static>(
|
||||
&self,
|
||||
value: T,
|
||||
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
|
||||
caller: &'static std::panic::Location<'static>,
|
||||
) -> GenerationalBox<T> {
|
||||
let mut location = self.store.claim();
|
||||
let key = location.replace_with_caller(
|
||||
value,
|
||||
#[cfg(any(debug_assertions, feature = "debug_borrows"))]
|
||||
caller,
|
||||
);
|
||||
self.owned.borrow_mut().push(location);
|
||||
key
|
||||
}
|
||||
|
@ -341,12 +700,16 @@ impl Owner {
|
|||
/// Creates an invalid handle. This is useful for creating a handle that will be filled in later. If you use this before the value is filled in, you will get may get a panic or an out of date value.
|
||||
pub fn invalid<T: 'static>(&self) -> GenerationalBox<T> {
|
||||
let location = self.store.claim();
|
||||
GenerationalBox {
|
||||
let key = GenerationalBox {
|
||||
raw: location,
|
||||
#[cfg(any(debug_assertions, feature = "check_generation"))]
|
||||
generation: location.generation.get(),
|
||||
generation: location.0.generation.get(),
|
||||
#[cfg(any(debug_assertions, feature = "debug_ownership"))]
|
||||
created_at: std::panic::Location::caller(),
|
||||
_marker: PhantomData,
|
||||
}
|
||||
};
|
||||
self.owned.borrow_mut().push(location);
|
||||
key
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
use dioxus_core::{ScopeState, TaskId};
|
||||
use std::{any::Any, cell::Cell, future::Future};
|
||||
use std::{
|
||||
any::Any,
|
||||
cell::{Cell, RefCell},
|
||||
future::Future,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use crate::UseFutureDep;
|
||||
|
||||
|
@ -14,7 +19,7 @@ use crate::UseFutureDep;
|
|||
/// ## Arguments
|
||||
///
|
||||
/// - `dependencies`: a tuple of references to values that are `PartialEq` + `Clone`.
|
||||
/// - `future`: a closure that takes the `dependencies` as arguments and returns a `'static` future.
|
||||
/// - `future`: a closure that takes the `dependencies` as arguments and returns a `'static` future. That future may return nothing or a closure that will be executed when the dependencies change to clean up the effect.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
|
@ -33,6 +38,16 @@ use crate::UseFutureDep;
|
|||
/// }
|
||||
/// });
|
||||
///
|
||||
/// // Only fetch the user data when the id changes.
|
||||
/// use_effect(cx, (id,), |(id,)| {
|
||||
/// to_owned![name];
|
||||
/// async move {
|
||||
/// let user = fetch_user(id).await;
|
||||
/// name.set(user.name);
|
||||
/// move || println!("Cleaning up from {}", id)
|
||||
/// }
|
||||
/// });
|
||||
///
|
||||
/// let name = name.get().clone().unwrap_or("Loading...".to_string());
|
||||
///
|
||||
/// render!(
|
||||
|
@ -45,34 +60,80 @@ use crate::UseFutureDep;
|
|||
/// render!(Profile { id: 0 })
|
||||
/// }
|
||||
/// ```
|
||||
pub fn use_effect<T, F, D>(cx: &ScopeState, dependencies: D, future: impl FnOnce(D::Out) -> F)
|
||||
pub fn use_effect<T, R, D>(cx: &ScopeState, dependencies: D, future: impl FnOnce(D::Out) -> R)
|
||||
where
|
||||
T: 'static,
|
||||
F: Future<Output = T> + 'static,
|
||||
D: UseFutureDep,
|
||||
R: UseEffectReturn<T>,
|
||||
{
|
||||
struct UseEffect {
|
||||
needs_regen: bool,
|
||||
task: Cell<Option<TaskId>>,
|
||||
dependencies: Vec<Box<dyn Any>>,
|
||||
cleanup: UseEffectCleanup,
|
||||
}
|
||||
|
||||
impl Drop for UseEffect {
|
||||
fn drop(&mut self) {
|
||||
if let Some(cleanup) = self.cleanup.borrow_mut().take() {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let state = cx.use_hook(move || UseEffect {
|
||||
needs_regen: true,
|
||||
task: Cell::new(None),
|
||||
dependencies: Vec::new(),
|
||||
cleanup: Rc::new(RefCell::new(None)),
|
||||
});
|
||||
|
||||
if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen {
|
||||
// Call the cleanup function if it exists
|
||||
if let Some(cleanup) = state.cleanup.borrow_mut().take() {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
// We don't need regen anymore
|
||||
state.needs_regen = false;
|
||||
|
||||
// Create the new future
|
||||
let fut = future(dependencies.out());
|
||||
let return_value = future(dependencies.out());
|
||||
|
||||
state.task.set(Some(cx.push_future(async move {
|
||||
fut.await;
|
||||
})));
|
||||
if let Some(task) = return_value.apply(state.cleanup.clone(), cx) {
|
||||
state.task.set(Some(task));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type UseEffectCleanup = Rc<RefCell<Option<Box<dyn FnOnce()>>>>;
|
||||
|
||||
/// Something that can be returned from a `use_effect` hook.
|
||||
pub trait UseEffectReturn<T> {
|
||||
fn apply(self, oncleanup: UseEffectCleanup, cx: &ScopeState) -> Option<TaskId>;
|
||||
}
|
||||
|
||||
impl<T> UseEffectReturn<()> for T
|
||||
where
|
||||
T: Future<Output = ()> + 'static,
|
||||
{
|
||||
fn apply(self, _: UseEffectCleanup, cx: &ScopeState) -> Option<TaskId> {
|
||||
Some(cx.push_future(self))
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct CleanupFutureMarker;
|
||||
impl<T, F> UseEffectReturn<CleanupFutureMarker> for T
|
||||
where
|
||||
T: Future<Output = F> + 'static,
|
||||
F: FnOnce() + 'static,
|
||||
{
|
||||
fn apply(self, oncleanup: UseEffectCleanup, cx: &ScopeState) -> Option<TaskId> {
|
||||
let task = cx.push_future(async move {
|
||||
let cleanup = self.await;
|
||||
*oncleanup.borrow_mut() = Some(Box::new(cleanup) as Box<dyn FnOnce()>);
|
||||
});
|
||||
Some(task)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ macro_rules! debug_location {
|
|||
}
|
||||
|
||||
pub mod error {
|
||||
#[cfg(debug_assertions)]
|
||||
fn locations_display(locations: &[&'static std::panic::Location<'static>]) -> String {
|
||||
locations
|
||||
.iter()
|
||||
|
|
|
@ -122,7 +122,7 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
|
|||
} = cfg;
|
||||
|
||||
if let Ok(crate_dir) = PathBuf::from_str(root_path) {
|
||||
// try to find the gitingore file
|
||||
// try to find the gitignore file
|
||||
let gitignore_file_path = crate_dir.join(".gitignore");
|
||||
let (gitignore, _) = ignore::gitignore::Gitignore::new(gitignore_file_path);
|
||||
|
||||
|
@ -152,21 +152,20 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
|
|||
}
|
||||
let file_map = Arc::new(Mutex::new(file_map));
|
||||
|
||||
let target_dir = crate_dir.join("target");
|
||||
let hot_reload_socket_path = target_dir.join("dioxusin");
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// On unix, if you force quit the application, it can leave the file socket open
|
||||
// This will cause the local socket listener to fail to open
|
||||
// We check if the file socket is already open from an old session and then delete it
|
||||
let paths = ["./dioxusin", "./@dioxusin"];
|
||||
for path in paths {
|
||||
let path = PathBuf::from(path);
|
||||
if path.exists() {
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
if hot_reload_socket_path.exists() {
|
||||
let _ = std::fs::remove_file(hot_reload_socket_path);
|
||||
}
|
||||
}
|
||||
|
||||
match LocalSocketListener::bind("@dioxusin") {
|
||||
match LocalSocketListener::bind(hot_reload_socket_path) {
|
||||
Ok(local_socket_stream) => {
|
||||
let aborted = Arc::new(Mutex::new(false));
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use std::io::{BufRead, BufReader};
|
||||
use std::{
|
||||
io::{BufRead, BufReader},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use dioxus_core::Template;
|
||||
#[cfg(feature = "file_watcher")]
|
||||
|
@ -24,7 +27,8 @@ pub enum HotReloadMsg {
|
|||
/// Connect to the hot reloading listener. The callback provided will be called every time a template change is detected
|
||||
pub fn connect(mut f: impl FnMut(HotReloadMsg) + Send + 'static) {
|
||||
std::thread::spawn(move || {
|
||||
if let Ok(socket) = LocalSocketStream::connect("@dioxusin") {
|
||||
let path = PathBuf::from("./").join("target").join("dioxusin");
|
||||
if let Ok(socket) = LocalSocketStream::connect(path) {
|
||||
let mut buf_reader = BufReader::new(socket);
|
||||
loop {
|
||||
let mut buf = String::new();
|
||||
|
|
|
@ -21,7 +21,7 @@ keyboard-types = "0.7"
|
|||
async-trait = "0.1.58"
|
||||
serde-value = "0.7.0"
|
||||
tokio = { workspace = true, features = ["fs", "io-util"], optional = true }
|
||||
rfd = { version = "0.11.3", optional = true }
|
||||
rfd = { version = "0.12", optional = true }
|
||||
async-channel = "1.8.0"
|
||||
serde_json = { version = "1", optional = true }
|
||||
|
||||
|
@ -68,3 +68,4 @@ mounted = [
|
|||
wasm-bind = ["web-sys", "wasm-bindgen"]
|
||||
native-bind = ["tokio"]
|
||||
hot-reload-context = ["dioxus-rsx"]
|
||||
html-to-rsx = []
|
||||
|
|
|
@ -74,7 +74,26 @@ macro_rules! impl_attribute_match {
|
|||
$attr:ident $fil:ident: $vil:ident (in $ns:literal),
|
||||
) => {
|
||||
if $attr == stringify!($fil) {
|
||||
return Some((stringify!(fil), Some(ns)));
|
||||
return Some((stringify!(fil), Some($ns)));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[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));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -180,14 +199,26 @@ macro_rules! impl_element_match {
|
|||
};
|
||||
|
||||
(
|
||||
$el:ident $name:ident $namespace:tt {
|
||||
$el:ident $name:ident $namespace:literal {
|
||||
$(
|
||||
$fil:ident: $vil:ident $extra:tt,
|
||||
)*
|
||||
}
|
||||
) => {
|
||||
if $el == stringify!($name) {
|
||||
return Some((stringify!($name), Some(stringify!($namespace))));
|
||||
return Some((stringify!($name), Some($namespace)));
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
$el:ident $name:ident [$_:literal, $namespace:tt] {
|
||||
$(
|
||||
$fil:ident: $vil:ident $extra:tt,
|
||||
)*
|
||||
}
|
||||
) => {
|
||||
if $el == stringify!($name) {
|
||||
return Some((stringify!($name), Some($namespace)));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -207,6 +238,8 @@ macro_rules! impl_element_match_attributes {
|
|||
$attr $fil: $vil ($extra),
|
||||
);
|
||||
)*
|
||||
|
||||
return impl_map_global_attributes!($el $attr $name None);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -223,10 +256,41 @@ macro_rules! impl_element_match_attributes {
|
|||
$attr $fil: $vil ($extra),
|
||||
);
|
||||
)*
|
||||
|
||||
return impl_map_global_attributes!($el $attr $name $namespace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hot-reload-context")]
|
||||
macro_rules! impl_map_global_attributes {
|
||||
(
|
||||
$el:ident $attr:ident $element:ident None
|
||||
) => {
|
||||
map_global_attributes($attr)
|
||||
};
|
||||
|
||||
(
|
||||
$el:ident $attr:ident $element:ident $namespace:literal
|
||||
) => {
|
||||
if $namespace == "http://www.w3.org/2000/svg" {
|
||||
map_svg_attributes($attr)
|
||||
} else {
|
||||
map_global_attributes($attr)
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
$el:ident $attr:ident $element:ident [$name:literal, $namespace:tt]
|
||||
) => {
|
||||
if $namespace == "http://www.w3.org/2000/svg" {
|
||||
map_svg_attributes($attr)
|
||||
} else {
|
||||
map_global_attributes($attr)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! builder_constructors {
|
||||
(
|
||||
$(
|
||||
|
@ -254,7 +318,7 @@ macro_rules! builder_constructors {
|
|||
}
|
||||
);
|
||||
)*
|
||||
map_global_attributes(attribute).or_else(|| map_svg_attributes(attribute))
|
||||
None
|
||||
}
|
||||
|
||||
fn map_element(element: &str) -> Option<(&'static str, Option<&'static str>)> {
|
||||
|
@ -271,6 +335,38 @@ macro_rules! builder_constructors {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "html-to-rsx")]
|
||||
pub fn map_html_attribute_to_rsx(html: &str) -> Option<&'static str> {
|
||||
$(
|
||||
$(
|
||||
impl_html_to_rsx_attribute_match!(
|
||||
html $fil $extra
|
||||
);
|
||||
)*
|
||||
)*
|
||||
|
||||
if let Some(name) = crate::map_html_global_attributes_to_rsx(html) {
|
||||
return Some(name);
|
||||
}
|
||||
|
||||
if let Some(name) = crate::map_html_svg_attributes_to_rsx(html) {
|
||||
return Some(name);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "html-to-rsx")]
|
||||
pub fn map_html_element_to_rsx(html: &str) -> Option<&'static str> {
|
||||
$(
|
||||
if html == stringify!($name) {
|
||||
return Some(stringify!($name));
|
||||
}
|
||||
)*
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
$(
|
||||
impl_element!(
|
||||
$(#[$attr])*
|
||||
|
@ -782,6 +878,7 @@ builder_constructors! {
|
|||
decoding: ImageDecoding DEFAULT,
|
||||
height: usize DEFAULT,
|
||||
ismap: Bool DEFAULT,
|
||||
loading: String DEFAULT,
|
||||
src: Uri DEFAULT,
|
||||
srcset: String DEFAULT, // FIXME this is much more complicated
|
||||
usemap: String DEFAULT, // FIXME should be a fragment starting with '#'
|
||||
|
@ -952,9 +1049,8 @@ builder_constructors! {
|
|||
src: Uri DEFAULT,
|
||||
text: String DEFAULT,
|
||||
|
||||
// r#async: Bool,
|
||||
// r#type: String, // TODO could be an enum
|
||||
r#type: String "type",
|
||||
r#async: Bool "async",
|
||||
r#type: String "type", // TODO could be an enum
|
||||
r#script: String "script",
|
||||
};
|
||||
|
||||
|
|
|
@ -119,10 +119,7 @@ impl_event! {
|
|||
/// oncontextmenu
|
||||
oncontextmenu
|
||||
|
||||
/// ondoubleclick
|
||||
ondoubleclick
|
||||
|
||||
/// ondoubleclick
|
||||
#[deprecated(since = "0.5.0", note = "use ondoubleclick instead")]
|
||||
ondblclick
|
||||
|
||||
/// onmousedown
|
||||
|
@ -149,6 +146,22 @@ impl_event! {
|
|||
onmouseup
|
||||
}
|
||||
|
||||
/// ondoubleclick
|
||||
#[inline]
|
||||
pub fn ondoubleclick<'a, E: crate::EventReturn<T>, T>(
|
||||
_cx: &'a ::dioxus_core::ScopeState,
|
||||
mut _f: impl FnMut(::dioxus_core::Event<MouseData>) -> E + 'a,
|
||||
) -> ::dioxus_core::Attribute<'a> {
|
||||
::dioxus_core::Attribute::new(
|
||||
"ondblclick",
|
||||
_cx.listener(move |e: ::dioxus_core::Event<MouseData>| {
|
||||
_f(e).spawn(_cx);
|
||||
}),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
impl MouseData {
|
||||
/// Construct MouseData with the specified properties
|
||||
///
|
||||
|
|
|
@ -33,12 +33,44 @@ macro_rules! trait_method_mapping {
|
|||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "html-to-rsx")]
|
||||
macro_rules! html_to_rsx_attribute_mapping {
|
||||
(
|
||||
$matching:ident;
|
||||
$(#[$attr:meta])*
|
||||
$name:ident;
|
||||
) => {
|
||||
if $matching == stringify!($name) {
|
||||
return Some(stringify!($name));
|
||||
}
|
||||
};
|
||||
(
|
||||
$matching:ident;
|
||||
$(#[$attr:meta])*
|
||||
$name:ident: $lit:literal;
|
||||
) => {
|
||||
if $matching == stringify!($lit) {
|
||||
return Some(stringify!($name));
|
||||
}
|
||||
};
|
||||
(
|
||||
$matching:ident;
|
||||
$(#[$attr:meta])*
|
||||
$name:ident: $lit:literal, $ns:literal;
|
||||
) => {
|
||||
if $matching == stringify!($lit) {
|
||||
return Some(stringify!($name));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! trait_methods {
|
||||
(
|
||||
@base
|
||||
$(#[$trait_attr:meta])*
|
||||
$trait:ident;
|
||||
$fn:ident;
|
||||
$fn_html_to_rsx:ident;
|
||||
$(
|
||||
$(#[$attr:meta])*
|
||||
$name:ident $(: $($arg:literal),*)*;
|
||||
|
@ -62,6 +94,18 @@ macro_rules! trait_methods {
|
|||
)*
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "html-to-rsx")]
|
||||
#[doc = "Converts an HTML attribute to an RSX attribute"]
|
||||
pub(crate) fn $fn_html_to_rsx(html: &str) -> Option<&'static str> {
|
||||
$(
|
||||
html_to_rsx_attribute_mapping! {
|
||||
html;
|
||||
$name$(: $($arg),*)*;
|
||||
}
|
||||
)*
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// Rename the incoming ident and apply a custom namespace
|
||||
|
@ -79,6 +123,7 @@ trait_methods! {
|
|||
|
||||
GlobalAttributes;
|
||||
map_global_attributes;
|
||||
map_html_global_attributes_to_rsx;
|
||||
|
||||
/// Prevent the default action for this element.
|
||||
///
|
||||
|
@ -1593,6 +1638,7 @@ trait_methods! {
|
|||
@base
|
||||
SvgAttributes;
|
||||
map_svg_attributes;
|
||||
map_html_svg_attributes_to_rsx;
|
||||
|
||||
/// Prevent the default action for this element.
|
||||
///
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
mod elements;
|
||||
#[cfg(feature = "hot-reload-context")]
|
||||
pub use elements::HtmlCtx;
|
||||
#[cfg(feature = "html-to-rsx")]
|
||||
pub use elements::{map_html_attribute_to_rsx, map_html_element_to_rsx};
|
||||
pub mod events;
|
||||
pub mod geometry;
|
||||
mod global_attributes;
|
||||
|
|
|
@ -34,10 +34,14 @@ warp = { version = "0.3.3", optional = true }
|
|||
axum = { version = "0.6.1", optional = true, features = ["ws"] }
|
||||
|
||||
# salvo
|
||||
salvo = { version = "0.44.1", optional = true, features = ["ws"] }
|
||||
salvo = { version = "0.63.0", optional = true, features = ["websocket"] }
|
||||
once_cell = "1.17.1"
|
||||
async-trait = "0.1.71"
|
||||
|
||||
# rocket
|
||||
rocket = { version = "0.5.0", optional = true }
|
||||
rocket_ws = { version = "0.1.0", optional = true }
|
||||
|
||||
# actix is ... complicated?
|
||||
# actix-files = { version = "0.6.2", optional = true }
|
||||
# actix-web = { version = "4.2.1", optional = true }
|
||||
|
@ -49,13 +53,16 @@ tokio = { workspace = true, features = ["full"] }
|
|||
dioxus = { workspace = true }
|
||||
warp = "0.3.3"
|
||||
axum = { version = "0.6.1", features = ["ws"] }
|
||||
salvo = { version = "0.44.1", features = ["affix", "ws"] }
|
||||
salvo = { version = "0.63.0", features = ["affix", "websocket"] }
|
||||
rocket = "0.5.0"
|
||||
rocket_ws = "0.1.0"
|
||||
tower = "0.4.13"
|
||||
|
||||
[features]
|
||||
default = ["hot-reload"]
|
||||
# actix = ["actix-files", "actix-web", "actix-ws"]
|
||||
hot-reload = ["dioxus-hot-reload"]
|
||||
rocket = ["dep:rocket", "dep:rocket_ws"]
|
||||
|
||||
[[example]]
|
||||
name = "axum"
|
||||
|
@ -68,3 +75,7 @@ required-features = ["salvo"]
|
|||
[[example]]
|
||||
name = "warp"
|
||||
required-features = ["warp"]
|
||||
|
||||
[[example]]
|
||||
name = "rocket"
|
||||
required-features = ["rocket"]
|
||||
|
|
|
@ -28,6 +28,7 @@ The current backend frameworks supported include:
|
|||
- Axum
|
||||
- Warp
|
||||
- Salvo
|
||||
- Rocket
|
||||
|
||||
Dioxus-LiveView exports some primitives to wire up an app into an existing backend framework.
|
||||
|
||||
|
|
76
packages/liveview/examples/rocket.rs
Normal file
76
packages/liveview/examples/rocket.rs
Normal file
|
@ -0,0 +1,76 @@
|
|||
#[macro_use]
|
||||
extern crate rocket;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_liveview::LiveViewPool;
|
||||
use rocket::response::content::RawHtml;
|
||||
use rocket::{Config, Rocket, State};
|
||||
use rocket_ws::{Channel, WebSocket};
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let mut num = use_state(cx, || 0);
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
"hello Rocket! {num}"
|
||||
button { onclick: move |_| num += 1, "Increment" }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn index_page_with_glue(glue: &str) -> RawHtml<String> {
|
||||
RawHtml(format!(
|
||||
r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head> <title>Dioxus LiveView with Rocket</title> </head>
|
||||
<body> <div id="main"></div> </body>
|
||||
{glue}
|
||||
</html>
|
||||
"#,
|
||||
glue = glue
|
||||
))
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn index(config: &Config) -> RawHtml<String> {
|
||||
index_page_with_glue(&dioxus_liveview::interpreter_glue(&format!(
|
||||
"ws://{addr}:{port}/ws",
|
||||
addr = config.address,
|
||||
port = config.port,
|
||||
)))
|
||||
}
|
||||
|
||||
#[get("/as-path")]
|
||||
async fn as_path() -> RawHtml<String> {
|
||||
index_page_with_glue(&dioxus_liveview::interpreter_glue("/ws"))
|
||||
}
|
||||
|
||||
#[get("/ws")]
|
||||
fn ws(ws: WebSocket, pool: &State<LiveViewPool>) -> Channel<'static> {
|
||||
let pool = pool.inner().to_owned();
|
||||
|
||||
ws.channel(move |stream| {
|
||||
Box::pin(async move {
|
||||
let _ = pool
|
||||
.launch(dioxus_liveview::rocket_socket(stream), app)
|
||||
.await;
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let view = dioxus_liveview::LiveViewPool::new();
|
||||
|
||||
Rocket::build()
|
||||
.manage(view)
|
||||
.mount("/", routes![index, as_path, ws])
|
||||
.ignite()
|
||||
.await
|
||||
.expect("Failed to ignite rocket")
|
||||
.launch()
|
||||
.await
|
||||
.expect("Failed to launch rocket");
|
||||
}
|
25
packages/liveview/src/adapters/rocket_adapter.rs
Normal file
25
packages/liveview/src/adapters/rocket_adapter.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
use crate::{LiveViewError, LiveViewSocket};
|
||||
use rocket::futures::{SinkExt, StreamExt};
|
||||
use rocket_ws::{result::Error, stream::DuplexStream, Message};
|
||||
|
||||
/// Convert a rocket websocket into a LiveViewSocket
|
||||
///
|
||||
/// This is required to launch a LiveView app using the rocket web framework
|
||||
pub fn rocket_socket(stream: DuplexStream) -> impl LiveViewSocket {
|
||||
stream
|
||||
.map(transform_rx)
|
||||
.with(transform_tx)
|
||||
.sink_map_err(|_| LiveViewError::SendingFailed)
|
||||
}
|
||||
|
||||
fn transform_rx(message: Result<Message, Error>) -> Result<Vec<u8>, LiveViewError> {
|
||||
message
|
||||
.map_err(|_| LiveViewError::SendingFailed)?
|
||||
.into_text()
|
||||
.map(|s| s.into_bytes())
|
||||
.map_err(|_| LiveViewError::SendingFailed)
|
||||
}
|
||||
|
||||
async fn transform_tx(message: Vec<u8>) -> Result<Message, Error> {
|
||||
Ok(Message::Text(String::from_utf8_lossy(&message).to_string()))
|
||||
}
|
|
@ -18,6 +18,11 @@ pub mod adapters {
|
|||
|
||||
#[cfg(feature = "salvo")]
|
||||
pub use salvo_adapter::*;
|
||||
|
||||
#[cfg(feature = "rocket")]
|
||||
pub mod rocket_adapter;
|
||||
#[cfg(feature = "rocket")]
|
||||
pub use rocket_adapter::*;
|
||||
}
|
||||
|
||||
pub use adapters::*;
|
||||
|
|
|
@ -57,7 +57,12 @@ impl DioxusState {
|
|||
node.insert(ElementIdComponent(element_id));
|
||||
if self.node_id_mapping.len() <= element_id.0 {
|
||||
self.node_id_mapping.resize(element_id.0 + 1, None);
|
||||
} else if let Some(mut node) =
|
||||
self.node_id_mapping[element_id.0].and_then(|id| node.real_dom_mut().get_mut(id))
|
||||
{
|
||||
node.remove();
|
||||
}
|
||||
|
||||
self.node_id_mapping[element_id.0] = Some(node_id);
|
||||
}
|
||||
|
||||
|
|
|
@ -92,10 +92,10 @@ impl State for TaffyLayout {
|
|||
attribute, value, ..
|
||||
} in attributes
|
||||
{
|
||||
if let Some(text) = value.as_text() {
|
||||
if value.as_custom().is_none() {
|
||||
apply_layout_attributes_cfg(
|
||||
&attribute.name,
|
||||
text,
|
||||
&value.to_string(),
|
||||
&mut style,
|
||||
&LayoutConfigeration {
|
||||
border_widths: BorderWidths {
|
||||
|
|
|
@ -75,6 +75,7 @@ impl Redirect {
|
|||
|
||||
let (segments, query) = parse_route_segments(
|
||||
path.span(),
|
||||
#[allow(clippy::map_identity)]
|
||||
closure_arguments.iter().map(|(name, ty)| (name, ty)),
|
||||
&path.value(),
|
||||
)?;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue