Merge branch 'master' into add-file-data-drag-event

This commit is contained in:
Evan Almloff committed 2023-09-17 18:56:59 -05:00
commit 847801908f
575 files changed
+12880 -17751

No files matched your search

+14 -8
View File
@@ -1,12 +1,12 @@
[package]
name = "dioxus-desktop"
version = "0.3.0"
version = { workspace = true }
authors = ["Jonathan Kelley"]
edition = "2018"
description = "WebView renderer for Dioxus"
license = "MIT/Apache-2.0"
license = "MIT OR Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com/docs/0.3/guide/en/getting_started/desktop.html"
homepage = "https://dioxuslabs.com/learn/0.4/getting_started/desktop"
keywords = ["dom", "ui", "gui", "react"]
[dependencies]
@@ -17,9 +17,9 @@ dioxus-hot-reload = { workspace = true, optional = true }
serde = "1.0.136"
serde_json = "1.0.79"
thiserror = "1.0.30"
log = { workspace = true }
wry = { version = "0.28.0" }
thiserror = { workspace = true }
wry = { version = "0.28.0", default-features = false, features = ["protocol", "file-drop"] }
tracing = { workspace = true }
futures-channel = { workspace = true }
tokio = { workspace = true, features = [
"sync",
@@ -36,6 +36,7 @@ slab = { workspace = true }
futures-util = { workspace = true }
urlencoding = "2.1.2"
async-trait = "0.1.68"
[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]
@@ -50,13 +51,18 @@ core-foundation = "0.9.3"
objc = "0.2.7"
[features]
default = ["tokio_runtime", "hot-reload"]
default = ["tokio_runtime", "hot-reload", "wry/objc-exception"]
tokio_runtime = ["tokio"]
fullscreen = ["wry/fullscreen"]
transparent = ["wry/transparent"]
tray = ["wry/tray"]
devtools = ["wry/devtools"]
dox = ["wry/dox"]
hot-reload = ["dioxus-hot-reload"]
[package.metadata.docs.rs]
default-features = false
features = [ "dox", "tokio_runtime", "hot-reload" ]
[dev-dependencies]
dioxus-core-macro = { workspace = true }
dioxus-hooks = { workspace = true }
+3 -9
View File
@@ -1,6 +1,5 @@
# Dioxus Desktop (webview)
[![Crates.io][crates-badge]][crates-url]
[![MIT licensed][mit-badge]][mit-url]
[![Build Status][actions-badge]][actions-url]
@@ -8,31 +7,25 @@
[crates-badge]: https://img.shields.io/crates/v/dioxus-desktop.svg
[crates-url]: https://crates.io/crates/dioxus-desktop
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
[discord-url]: https://discord.gg/XgGxMSkvUM
[Website](https://dioxuslabs.com) |
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
[Guides](https://dioxuslabs.com/learn/0.4/) |
[API Docs](https://docs.rs/dioxus-desktop/latest/dioxus_desktop) |
[Chat](https://discord.gg/XgGxMSkvUM)
## Overview
`dioxus-desktop` provides a webview-based desktop renderer for the Dioxus VirtualDom.
This requires that webview is installed on the target system. WebView is installed by default on macOS and iOS devices, but might not come preinstalled on Windows or Linux devices. To fix these issues, follow the [instructions in the guide](guide-url).
[guide-url]: https://dioxuslabs.com/docs/0.3/guide/en/getting_started/desktop.html
[guide-url]: https://dioxuslabs.com/learn/0.4/getting_started/desktop
## Features
@@ -48,6 +41,7 @@ This requires that webview is installed on the target system. WebView is install
- Join the discord and ask questions!
## License
This project is licensed under the [MIT license].
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
+103 -100
View File
@@ -29,21 +29,24 @@ pub fn main() {
}
fn mock_event(cx: &ScopeState, id: &'static str, value: &'static str) {
use_effect(cx, (), |_| {
let desktop_context: DesktopContext = cx.consume_context().unwrap();
async move {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
desktop_context.eval(&format!(
r#"let element = document.getElementById('{}');
let eval_provider = use_eval(cx).clone();
use_effect(cx, (), move |_| async move {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
let js = format!(
r#"
//console.log("ran");
// Dispatch a synthetic event
const event = {};
let event = {};
let element = document.getElementById('{}');
console.log(element, event);
element.dispatchEvent(event);
"#,
id, value
));
}
});
value, id
);
eval_provider(&js).unwrap();
})
}
#[allow(deprecated)]
@@ -56,149 +59,149 @@ fn app(cx: Scope) -> Element {
cx,
"button",
r#"new MouseEvent("click", {
view: window,
bubbles: true,
cancelable: true,
button: 0,
})"#,
view: window,
bubbles: true,
cancelable: true,
button: 0,
})"#,
);
// mouse_move_div
mock_event(
cx,
"mouse_move_div",
r#"new MouseEvent("mousemove", {
view: window,
bubbles: true,
cancelable: true,
buttons: 2,
})"#,
view: window,
bubbles: true,
cancelable: true,
buttons: 2,
})"#,
);
// mouse_click_div
mock_event(
cx,
"mouse_click_div",
r#"new MouseEvent("click", {
view: window,
bubbles: true,
cancelable: true,
buttons: 2,
button: 2,
})"#,
view: window,
bubbles: true,
cancelable: true,
buttons: 2,
button: 2,
})"#,
);
// mouse_dblclick_div
mock_event(
cx,
"mouse_dblclick_div",
r#"new MouseEvent("dblclick", {
view: window,
bubbles: true,
cancelable: true,
buttons: 1|2,
button: 2,
})"#,
view: window,
bubbles: true,
cancelable: true,
buttons: 1|2,
button: 2,
})"#,
);
// mouse_down_div
mock_event(
cx,
"mouse_down_div",
r#"new MouseEvent("mousedown", {
view: window,
bubbles: true,
cancelable: true,
buttons: 2,
button: 2,
})"#,
view: window,
bubbles: true,
cancelable: true,
buttons: 2,
button: 2,
})"#,
);
// mouse_up_div
mock_event(
cx,
"mouse_up_div",
r#"new MouseEvent("mouseup", {
view: window,
bubbles: true,
cancelable: true,
buttons: 0,
button: 0,
})"#,
view: window,
bubbles: true,
cancelable: true,
buttons: 0,
button: 0,
})"#,
);
// wheel_div
mock_event(
cx,
"wheel_div",
r#"new WheelEvent("wheel", {
view: window,
deltaX: 1.0,
deltaY: 2.0,
deltaZ: 3.0,
deltaMode: 0x00,
bubbles: true,
})"#,
view: window,
deltaX: 1.0,
deltaY: 2.0,
deltaZ: 3.0,
deltaMode: 0x00,
bubbles: true,
})"#,
);
// key_down_div
mock_event(
cx,
"key_down_div",
r#"new KeyboardEvent("keydown", {
key: "a",
code: "KeyA",
location: 0,
repeat: true,
keyCode: 65,
charCode: 97,
char: "a",
charCode: 0,
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
isComposing: false,
which: 65,
bubbles: true,
})"#,
key: "a",
code: "KeyA",
location: 0,
repeat: true,
keyCode: 65,
charCode: 97,
char: "a",
charCode: 0,
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
isComposing: false,
which: 65,
bubbles: true,
})"#,
);
// key_up_div
mock_event(
cx,
"key_up_div",
r#"new KeyboardEvent("keyup", {
key: "a",
code: "KeyA",
location: 0,
repeat: false,
keyCode: 65,
charCode: 97,
char: "a",
charCode: 0,
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
isComposing: false,
which: 65,
bubbles: true,
})"#,
key: "a",
code: "KeyA",
location: 0,
repeat: false,
keyCode: 65,
charCode: 97,
char: "a",
charCode: 0,
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
isComposing: false,
which: 65,
bubbles: true,
})"#,
);
// key_press_div
mock_event(
cx,
"key_press_div",
r#"new KeyboardEvent("keypress", {
key: "a",
code: "KeyA",
location: 0,
repeat: false,
keyCode: 65,
charCode: 97,
char: "a",
charCode: 0,
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
isComposing: false,
which: 65,
bubbles: true,
})"#,
key: "a",
code: "KeyA",
location: 0,
repeat: false,
keyCode: 65,
charCode: 97,
char: "a",
charCode: 0,
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
isComposing: false,
which: 65,
bubbles: true,
})"#,
);
// focus_in_div
mock_event(
+16 -14
View File
@@ -27,20 +27,20 @@ fn main() {
}
fn use_inner_html(cx: &ScopeState, id: &'static str) -> Option<String> {
let eval_provider = use_eval(cx);
let value: &UseRef<Option<String>> = use_ref(cx, || None);
use_effect(cx, (), |_| {
to_owned![value];
let desktop_context: DesktopContext = cx.consume_context().unwrap();
to_owned![value, eval_provider];
async move {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
let html = desktop_context
.eval(&format!(
r#"let element = document.getElementById('{}');
return element.innerHTML;"#,
id
))
.await;
if let Ok(serde_json::Value::String(html)) = html {
let html = eval_provider(&format!(
r#"let element = document.getElementById('{}');
return element.innerHTML"#,
id
))
.unwrap();
if let Ok(serde_json::Value::String(html)) = html.await {
println!("html: {}", html);
value.set(Some(html));
}
@@ -53,13 +53,15 @@ const EXPECTED_HTML: &str = r#"<div id="5" style="width: 100px; height: 100px; c
fn check_html_renders(cx: Scope) -> Element {
let inner_html = use_inner_html(cx, "main_div");
let desktop_context: DesktopContext = cx.consume_context().unwrap();
if let Some(raw_html) = inner_html.as_deref() {
let fragment = scraper::Html::parse_fragment(raw_html);
println!("fragment: {:?}", fragment.html());
if let Some(raw_html) = inner_html {
println!("{}", raw_html);
let fragment = scraper::Html::parse_fragment(&raw_html);
println!("fragment: {}", fragment.html());
let expected = scraper::Html::parse_fragment(EXPECTED_HTML);
println!("fragment: {:?}", expected.html());
println!("expected: {}", expected.html());
if fragment == expected {
println!("html matches");
desktop_context.close();
+2 -11
View File
@@ -3,7 +3,6 @@ use std::rc::Rc;
use std::rc::Weak;
use crate::create_new_window;
use crate::eval::EvalResult;
use crate::events::IpcMessage;
use crate::query::QueryEngine;
use crate::shortcut::ShortcutId;
@@ -195,7 +194,7 @@ impl DesktopService {
/// launch print modal
pub fn print(&self) {
if let Err(e) = self.webview.print() {
log::warn!("Open print modal failed: {e}");
tracing::warn!("Open print modal failed: {e}");
}
}
@@ -210,15 +209,7 @@ impl DesktopService {
self.webview.open_devtools();
#[cfg(not(debug_assertions))]
log::warn!("Devtools are disabled in release builds");
}
/// Evaluate a javascript expression
pub fn eval(&self, code: &str) -> EvalResult {
// the query id lets us keep track of the eval result and send it back to the main thread
let query = self.query.new_query(code, &self.webview);
EvalResult::new(query)
tracing::warn!("Devtools are disabled in release builds");
}
/// Create a wry event handler that listens for wry events.
+3 -3
View File
@@ -34,7 +34,7 @@ impl RenderedElementBacking for DesktopElement {
let fut = self
.query
.new_query::<Option<Rect<f64, f64>>>(&script, &self.webview.webview)
.new_query::<Option<Rect<f64, f64>>>(&script, self.webview.clone())
.resolve();
Box::pin(async move {
match fut.await {
@@ -61,7 +61,7 @@ impl RenderedElementBacking for DesktopElement {
let fut = self
.query
.new_query::<bool>(&script, &self.webview.webview)
.new_query::<bool>(&script, self.webview.clone())
.resolve();
Box::pin(async move {
match fut.await {
@@ -87,7 +87,7 @@ impl RenderedElementBacking for DesktopElement {
let fut = self
.query
.new_query::<bool>(&script, &self.webview.webview)
.new_query::<bool>(&script, self.webview.clone())
.resolve();
Box::pin(async move {
+56 -27
View File
@@ -1,41 +1,70 @@
use std::rc::Rc;
use crate::query::Query;
use crate::query::QueryError;
use crate::use_window;
#![allow(clippy::await_holding_refcell_ref)]
use async_trait::async_trait;
use dioxus_core::ScopeState;
use std::future::Future;
use std::future::IntoFuture;
use std::pin::Pin;
use dioxus_html::prelude::{EvalError, EvalProvider, Evaluator};
use std::{cell::RefCell, rc::Rc};
/// A future that resolves to the result of a JavaScript evaluation.
pub struct EvalResult {
pub(crate) query: Query<serde_json::Value>,
use crate::{query::Query, DesktopContext};
/// Provides the DesktopEvalProvider through [`cx.provide_context`].
pub fn init_eval(cx: &ScopeState) {
let desktop_ctx = cx.consume_context::<DesktopContext>().unwrap();
let provider: Rc<dyn EvalProvider> = Rc::new(DesktopEvalProvider { desktop_ctx });
cx.provide_context(provider);
}
impl EvalResult {
pub(crate) fn new(query: Query<serde_json::Value>) -> Self {
Self { query }
/// Reprents the desktop-target's provider of evaluators.
pub struct DesktopEvalProvider {
desktop_ctx: DesktopContext,
}
impl EvalProvider for DesktopEvalProvider {
fn new_evaluator(&self, js: String) -> Result<Rc<dyn Evaluator>, EvalError> {
Ok(Rc::new(DesktopEvaluator::new(self.desktop_ctx.clone(), js)))
}
}
impl IntoFuture for EvalResult {
type Output = Result<serde_json::Value, QueryError>;
/// Reprents a desktop-target's JavaScript evaluator.
pub struct DesktopEvaluator {
query: Rc<RefCell<Query<serde_json::Value>>>,
}
type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, QueryError>>>>;
impl DesktopEvaluator {
/// Creates a new evaluator for desktop-based targets.
pub fn new(desktop_ctx: DesktopContext, js: String) -> Self {
let ctx = desktop_ctx.clone();
let query = desktop_ctx.query.new_query(&js, ctx);
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.query.resolve())
as Pin<Box<dyn Future<Output = Result<serde_json::Value, QueryError>>>>
Self {
query: Rc::new(RefCell::new(query)),
}
}
}
/// Get a closure that executes any JavaScript in the WebView context.
pub fn use_eval(cx: &ScopeState) -> &Rc<dyn Fn(String) -> EvalResult> {
let desktop = use_window(cx);
&*cx.use_hook(|| {
let desktop = desktop.clone();
#[async_trait(?Send)]
impl Evaluator for DesktopEvaluator {
async fn join(&self) -> Result<serde_json::Value, EvalError> {
self.query
.borrow_mut()
.result()
.await
.map_err(|e| EvalError::Communication(e.to_string()))
}
Rc::new(move |script: String| desktop.eval(&script)) as Rc<dyn Fn(String) -> EvalResult>
})
/// Sends a message to the evaluated JavaScript.
fn send(&self, data: serde_json::Value) -> Result<(), EvalError> {
if let Err(e) = self.query.borrow_mut().send(data) {
return Err(EvalError::Communication(e.to_string()));
}
Ok(())
}
/// Gets an UnboundedReceiver to receive messages from the evaluated JavaScript.
async fn recv(&self) -> Result<serde_json::Value, EvalError> {
self.query
.borrow_mut()
.recv()
.await
.map_err(|e| EvalError::Communication(e.to_string()))
}
}
+39 -36
View File
@@ -14,42 +14,6 @@ pub(crate) struct FileDialogRequest {
pub bubbles: bool,
}
fn get_file_event_for_folder(request: &FileDialogRequest, dialog: rfd::FileDialog) -> Vec<PathBuf> {
if request.multiple {
dialog.pick_folders().into_iter().flatten().collect()
} else {
dialog.pick_folder().into_iter().collect()
}
}
fn get_file_event_for_file(
request: &FileDialogRequest,
mut dialog: rfd::FileDialog,
) -> Vec<PathBuf> {
let filters: Vec<_> = request
.accept
.as_deref()
.unwrap_or_default()
.split(',')
.filter_map(|s| Filters::from_str(s).ok())
.collect();
let file_extensions: Vec<_> = filters
.iter()
.flat_map(|f| f.as_extensions().into_iter())
.collect();
dialog = dialog.add_filter("name", file_extensions.as_slice());
let files: Vec<_> = if request.multiple {
dialog.pick_files().into_iter().flatten().collect()
} else {
dialog.pick_file().into_iter().collect()
};
files
}
#[cfg(not(any(
target_os = "windows",
target_os = "macos",
@@ -73,6 +37,45 @@ pub(crate) fn get_file_event(_request: &FileDialogRequest) -> Vec<PathBuf> {
target_os = "openbsd"
))]
pub(crate) fn get_file_event(request: &FileDialogRequest) -> Vec<PathBuf> {
fn get_file_event_for_folder(
request: &FileDialogRequest,
dialog: rfd::FileDialog,
) -> Vec<PathBuf> {
if request.multiple {
dialog.pick_folders().into_iter().flatten().collect()
} else {
dialog.pick_folder().into_iter().collect()
}
}
fn get_file_event_for_file(
request: &FileDialogRequest,
mut dialog: rfd::FileDialog,
) -> Vec<PathBuf> {
let filters: Vec<_> = request
.accept
.as_deref()
.unwrap_or_default()
.split(',')
.filter_map(|s| Filters::from_str(s).ok())
.collect();
let file_extensions: Vec<_> = filters
.iter()
.flat_map(|f| f.as_extensions().into_iter())
.collect();
dialog = dialog.add_filter("name", file_extensions.as_slice());
let files: Vec<_> = if request.multiple {
dialog.pick_files().into_iter().flatten().collect()
} else {
dialog.pick_file().into_iter().collect()
};
files
}
let dialog = rfd::FileDialog::new();
if request.directory {
+11 -5
View File
@@ -30,7 +30,7 @@ use dioxus_core::*;
use dioxus_html::MountedData;
use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent};
use element::DesktopElement;
pub use eval::{use_eval, EvalResult};
use eval::init_eval;
use futures_util::{pin_mut, FutureExt};
use shortcut::ShortcutRegistry;
pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
@@ -204,15 +204,17 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
},
Event::NewEvents(StartCause::Init) => {
//
let props = props.take().unwrap();
let cfg = cfg.take().unwrap();
// Create a dom
let dom = VirtualDom::new_with_props(root, props);
let handler = create_new_window(
cfg,
event_loop,
&proxy,
VirtualDom::new_with_props(root, props),
dom,
&queue,
&event_handlers,
shortcut_manager.clone(),
@@ -328,7 +330,7 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
if temp.contains_key("href") {
let open = webbrowser::open(temp["href"].as_str().unwrap());
if let Err(e) = open {
log::error!("Open Browser error: {:?}", e);
tracing::error!("Open Browser error: {:?}", e);
}
}
}
@@ -389,7 +391,11 @@ fn create_new_window(
shortcut_manager,
));
dom.base_scope().provide_context(desktop_context.clone());
let cx = dom.base_scope();
cx.provide_context(desktop_context.clone());
// Init eval
init_eval(cx);
WebviewHandler {
// We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both
+7 -2
View File
@@ -158,8 +158,13 @@ fn get_mime_from_path(trimmed: &Path) -> Result<&'static str> {
}
let res = match infer::get_from_path(trimmed)?.map(|f| f.mime_type()) {
Some(t) if t == "text/plain" => get_mime_by_ext(trimmed),
Some(f) => f,
Some(f) => {
if f == "text/plain" {
get_mime_by_ext(trimmed)
} else {
f
}
}
None => get_mime_by_ext(trimmed),
};
+195 -48
View File
@@ -1,110 +1,257 @@
use std::{cell::RefCell, rc::Rc};
use crate::DesktopContext;
use serde::{de::DeserializeOwned, Deserialize};
use serde_json::Value;
use slab::Slab;
use thiserror::Error;
use tokio::sync::broadcast::error::RecvError;
use wry::webview::WebView;
const DIOXUS_CODE: &str = r#"
let dioxus = {
recv: function () {
return new Promise((resolve, _reject) => {
// Ever 50 ms check for new data
let timeout = setTimeout(() => {
let __msg = null;
while (true) {
let __data = _message_queue.shift();
if (__data) {
__msg = __data;
break;
}
}
clearTimeout(timeout);
resolve(__msg);
}, 50);
});
},
send: function (value) {
window.ipc.postMessage(
JSON.stringify({
"method":"query",
"params": {
"id": _request_id,
"data": value,
"returned_value": false
}
})
);
}
}"#;
/// Tracks what query ids are currently active
#[derive(Default, Clone)]
struct SharedSlab {
slab: Rc<RefCell<Slab<()>>>,
struct SharedSlab<T = ()> {
slab: Rc<RefCell<Slab<T>>>,
}
/// Handles sending and receiving arbitrary queries from the webview. Queries can be resolved non-sequentially, so we use ids to track them.
#[derive(Clone)]
pub(crate) struct QueryEngine {
sender: Rc<tokio::sync::broadcast::Sender<QueryResult>>,
active_requests: SharedSlab,
}
impl Default for QueryEngine {
fn default() -> Self {
let (sender, _) = tokio::sync::broadcast::channel(1000);
impl<T> Clone for SharedSlab<T> {
fn clone(&self) -> Self {
Self {
sender: Rc::new(sender),
active_requests: SharedSlab::default(),
slab: self.slab.clone(),
}
}
}
impl<T> Default for SharedSlab<T> {
fn default() -> Self {
SharedSlab {
slab: Rc::new(RefCell::new(Slab::new())),
}
}
}
struct QueryEntry {
channel_sender: tokio::sync::mpsc::UnboundedSender<Value>,
return_sender: Option<tokio::sync::oneshot::Sender<Value>>,
}
const QUEUE_NAME: &str = "__msg_queues";
/// Handles sending and receiving arbitrary queries from the webview. Queries can be resolved non-sequentially, so we use ids to track them.
#[derive(Clone, Default)]
pub(crate) struct QueryEngine {
active_requests: SharedSlab<QueryEntry>,
}
impl QueryEngine {
/// Creates a new query and returns a handle to it. The query will be resolved when the webview returns a result with the same id.
pub fn new_query<V: DeserializeOwned>(&self, script: &str, webview: &WebView) -> Query<V> {
let request_id = self.active_requests.slab.borrow_mut().insert(());
pub fn new_query<V: DeserializeOwned>(
&self,
script: &str,
context: DesktopContext,
) -> Query<V> {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let (return_tx, return_rx) = tokio::sync::oneshot::channel();
let request_id = self.active_requests.slab.borrow_mut().insert(QueryEntry {
channel_sender: tx,
return_sender: Some(return_tx),
});
// start the query
// We embed the return of the eval in a function so we can send it back to the main thread
if let Err(err) = webview.evaluate_script(&format!(
r#"window.ipc.postMessage(
JSON.stringify({{
"method":"query",
"params": {{
"id": {request_id},
"data": (function(){{{script}}})()
if let Err(err) = context.webview.evaluate_script(&format!(
r#"(function(){{
(async (resolve, _reject) => {{
{DIOXUS_CODE}
if (!window.{QUEUE_NAME}) {{
window.{QUEUE_NAME} = [];
}}
let _request_id = {request_id};
if (!window.{QUEUE_NAME}[{request_id}]) {{
window.{QUEUE_NAME}[{request_id}] = [];
}}
let _message_queue = window.{QUEUE_NAME}[{request_id}];
{script}
}})().then((result)=>{{
let returned_value = {{
"method":"query",
"params": {{
"id": {request_id},
"data": result,
"returned_value": true
}}
}};
window.ipc.postMessage(
JSON.stringify(returned_value)
);
}})
);"#
}})();"#
)) {
log::warn!("Query error: {err}");
tracing::warn!("Query error: {err}");
}
Query {
slab: self.active_requests.clone(),
id: request_id,
reciever: self.sender.subscribe(),
receiver: rx,
return_receiver: Some(return_rx),
desktop: context,
phantom: std::marker::PhantomData,
}
}
/// Send a query result
/// Send a query channel message to the correct query
pub fn send(&self, data: QueryResult) {
let _ = self.sender.send(data);
let QueryResult {
id,
data,
returned_value,
} = data;
let mut slab = self.active_requests.slab.borrow_mut();
if let Some(entry) = slab.get_mut(id) {
if returned_value {
if let Some(sender) = entry.return_sender.take() {
let _ = sender.send(data);
}
} else {
let _ = entry.channel_sender.send(data);
}
}
}
}
pub(crate) struct Query<V: DeserializeOwned> {
slab: SharedSlab,
desktop: DesktopContext,
slab: SharedSlab<QueryEntry>,
receiver: tokio::sync::mpsc::UnboundedReceiver<Value>,
return_receiver: Option<tokio::sync::oneshot::Receiver<Value>>,
id: usize,
reciever: tokio::sync::broadcast::Receiver<QueryResult>,
phantom: std::marker::PhantomData<V>,
}
impl<V: DeserializeOwned> Query<V> {
/// Resolve the query
pub async fn resolve(mut self) -> Result<V, QueryError> {
let result = loop {
match self.reciever.recv().await {
Ok(result) => {
if result.id == self.id {
break V::deserialize(result.data).map_err(QueryError::DeserializeError);
}
}
Err(err) => {
break Err(QueryError::RecvError(err));
}
}
};
let result = self.result().await?;
V::deserialize(result).map_err(QueryError::Deserialize)
}
// Remove the query from the slab
/// Send a message to the query
pub fn send<S: ToString>(&self, message: S) -> Result<(), QueryError> {
let queue_id = self.id;
let data = message.to_string();
let script = format!(
r#"
if (!window.{QUEUE_NAME}) {{
window.{QUEUE_NAME} = [];
}}
if (!window.{QUEUE_NAME}[{queue_id}]) {{
window.{QUEUE_NAME}[{queue_id}] = [];
}}
window.{QUEUE_NAME}[{queue_id}].push({data});
"#
);
self.desktop
.webview
.evaluate_script(&script)
.map_err(|e| QueryError::Send(e.to_string()))?;
Ok(())
}
/// Receive a message from the query
pub async fn recv(&mut self) -> Result<Value, QueryError> {
self.receiver
.recv()
.await
.ok_or(QueryError::Recv(RecvError::Closed))
}
/// Receive the result of the query
pub async fn result(&mut self) -> Result<Value, QueryError> {
match self.return_receiver.take() {
Some(receiver) => receiver
.await
.map_err(|_| QueryError::Recv(RecvError::Closed)),
None => Err(QueryError::Finished),
}
}
}
impl<V: DeserializeOwned> Drop for Query<V> {
fn drop(&mut self) {
self.slab.slab.borrow_mut().remove(self.id);
let queue_id = self.id;
result
_ = self.desktop.webview.evaluate_script(&format!(
r#"
if (!window.{QUEUE_NAME}) {{
window.{QUEUE_NAME} = [];
}}
if (window.{QUEUE_NAME}[{queue_id}]) {{
window.{QUEUE_NAME}[{queue_id}] = [];
}}
"#
));
}
}
#[derive(Error, Debug)]
pub enum QueryError {
#[error("Error receiving query result: {0}")]
RecvError(RecvError),
Recv(RecvError),
#[error("Error sending message to query: {0}")]
Send(String),
#[error("Error deserializing query result: {0}")]
DeserializeError(serde_json::Error),
Deserialize(serde_json::Error),
#[error("Query has already been resolved")]
Finished,
}
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct QueryResult {
id: usize,
data: Value,
#[serde(default)]
returned_value: bool,
}
+1 -3
View File
@@ -8,7 +8,6 @@ One of Dioxus' flagship features is the ability to quickly build a native deskto
Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over the menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao) directly. An upcoming release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc.
## Getting Set up
Getting Set up with Dioxus-Desktop is quite easy. Make sure you have Rust and Cargo installed, and then create a new project:
@@ -44,9 +43,8 @@ fn app(cx: Scope) -> Element {
}
```
To configure the webview, menubar, and other important desktop-specific features, checkout out some of the launch configuration in the [API reference](https://docs.rs/dioxus-desktop/).
## Future Steps
Make sure to read the [Dioxus Guide](https://dioxuslabs.com/docs/0.3/guide/en) if you already haven't!
Make sure to read the [Dioxus Guide](https://dioxuslabs.com/learn/0.4/) if you already haven't!