chore(deps): upgrade wry from 0.45.0 to v0.52.0 (#4255)
* chore(deps): upgrade wry from 0.45.0 to v0.51.2 * Unwrap webview * update tauri * fix more versions * switch to a single websocket per app to communicate edits in desktop * use websockets on the ts side * queue pending edits before the websocket connects * fix decoding the websocket blobs in ts * rebuild js * fix clippy * more sensible struct names * allow http on android and require a key to match between the webview and native sides * remove out.txt * clippy * small style changes * More light cleanups --------- Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com> Co-authored-by: Evan Almloff <evanalmloff@gmail.com>
This commit is contained in:
18 files changed
+1324
-1083
No files matched your search
Generated
+857
-707
File diff suppressed because it is too large.
Load diff
+10
-10
@@ -226,10 +226,10 @@ html_parser = "0.7.0"
|
||||
thiserror = "2.0.12"
|
||||
prettyplease = { version = "0.2.30", features = ["verbatim"] }
|
||||
const_format = "0.2.34"
|
||||
cargo_toml = { version = "0.21.0" }
|
||||
tauri-utils = { version = "=2.4.0" }
|
||||
tauri-bundler = { version = "=2.4.0" }
|
||||
lru = "0.13.0"
|
||||
cargo_toml = { version = "0.22.1" }
|
||||
tauri-utils = { version = "=2.5.0" }
|
||||
tauri-bundler = { version = "=2.5.0" }
|
||||
lru = "0.14.0"
|
||||
async-trait = "0.1.87"
|
||||
axum = { version = "0.8.1", default-features = false }
|
||||
axum-server = { version = "0.7.1", default-features = false }
|
||||
@@ -264,8 +264,8 @@ ciborium = "0.2.2"
|
||||
base64 = "0.22.1"
|
||||
uuid = "1.15.1"
|
||||
convert_case = "0.8.0"
|
||||
tungstenite = { version = "0.26.2" }
|
||||
tokio-tungstenite = { version = "0.26.2" }
|
||||
tungstenite = { version = "0.27.0" }
|
||||
tokio-tungstenite = { version = "0.27.0" }
|
||||
gloo-timers = "0.3.0"
|
||||
fluent-uri = { version = "0.3.2", features = ["serde"] }
|
||||
internment = { version = "0.8.6" }
|
||||
@@ -313,16 +313,16 @@ memmap2 = "0.9.5"
|
||||
memfd = "0.6.4"
|
||||
|
||||
# desktop
|
||||
wry = { version = "0.45.0", default-features = false }
|
||||
tao = { version = "0.33.0", features = ["rwh_05"] }
|
||||
wry = { version = "0.52.1", default-features = false }
|
||||
tao = { version = "0.34.0", features = ["rwh_05"] }
|
||||
infer = "0.19.0"
|
||||
dunce = "1.0.5"
|
||||
percent-encoding = "2.3.1"
|
||||
global-hotkey = "0.7.0"
|
||||
rfd = { version = "0.15.2", default-features = false }
|
||||
muda = "0.16.1"
|
||||
muda = "0.17.0"
|
||||
cocoa = "0.26"
|
||||
core-foundation = "0.10.0"
|
||||
core-foundation = "0.10.1"
|
||||
objc = { version = "0.2.7", features = ["exception"] }
|
||||
objc_id = "0.1.1"
|
||||
tray-icon = "0.20.0"
|
||||
|
||||
@@ -26,7 +26,12 @@ fn app() -> Element {
|
||||
}
|
||||
|
||||
fn popup() -> Element {
|
||||
let mut count = use_signal(|| 0);
|
||||
rsx! {
|
||||
div { "This is a popup window!" }
|
||||
div {
|
||||
h1 { "Popup Window" }
|
||||
p { "Count: {count}" }
|
||||
button { onclick: move |_| count += 1, "Increment" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,9 @@
|
||||
<application android:hasCode="true" android:supportsRtl="true" android:icon="@mipmap/ic_launcher"
|
||||
android:extractNativeLibs="true"
|
||||
android:allowNativeHeapPointerTagging="false"
|
||||
android:label="@string/app_name" android:theme="@style/AppTheme">
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
<activity android:configChanges="orientation|screenLayout|screenSize|keyboardHidden" android:exported="true"
|
||||
android:label="@string/app_name" android:name="dev.dioxus.main.MainActivity">
|
||||
<meta-data android:name="android.app.lib_name" android:value="dioxusmain" />
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">127.0.0.1</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
@@ -2705,6 +2705,14 @@ impl BuildRequest {
|
||||
include_bytes!("../../assets/android/gen/app/src/main/res/values/styles.xml"),
|
||||
)?;
|
||||
|
||||
create_dir_all(res.join("xml"))?;
|
||||
write(
|
||||
res.join("xml").join("network_security_config.xml"),
|
||||
include_bytes!(
|
||||
"../../assets/android/gen/app/src/main/res/xml/network_security_config.xml"
|
||||
),
|
||||
)?;
|
||||
|
||||
create_dir_all(res.join("drawable"))?;
|
||||
write(
|
||||
res.join("drawable").join("ic_launcher_background.xml"),
|
||||
|
||||
@@ -114,6 +114,7 @@ impl From<MacOsSettings> for tauri_bundler::MacOsSettings {
|
||||
files: val.files,
|
||||
hardened_runtime: val.hardened_runtime,
|
||||
bundle_version: val.bundle_version,
|
||||
bundle_name: val.bundle_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ pub(crate) struct MacOsSettings {
|
||||
pub(crate) provider_short_name: Option<String>,
|
||||
pub(crate) entitlements: Option<String>,
|
||||
pub(crate) info_plist_path: Option<PathBuf>,
|
||||
pub(crate) bundle_name: Option<String>,
|
||||
/// List of custom files to add to the application bundle.
|
||||
/// Maps the path in the Contents directory in the app to the path of the file to include (relative to the current working directory).
|
||||
pub files: HashMap<PathBuf, PathBuf>,
|
||||
|
||||
@@ -19,7 +19,6 @@ dioxus-interpreter-js = { workspace = true, features = ["binary-protocol", "seri
|
||||
dioxus-cli-config = { workspace = true }
|
||||
dioxus-asset-resolver = { workspace = true }
|
||||
generational-box = { workspace = true }
|
||||
# hotreload only works on desktop platforms.... mobile is still wip
|
||||
dioxus-devtools = { workspace = true, optional = true }
|
||||
|
||||
serde = "1.0.218"
|
||||
@@ -50,6 +49,9 @@ dioxus-history = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
open = { workspace = true }
|
||||
libc = "0.2.170"
|
||||
pollster = "0.4.0"
|
||||
rand = { workspace = true, features = ["std_rng"] }
|
||||
subtle = { version = "2.6", features = ["const-generics"] }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
signal-hook = "0.3.17"
|
||||
@@ -71,7 +73,7 @@ objc_id = "0.1.1"
|
||||
|
||||
# use rustls on android
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
tokio-tungstenite = { workspace = true, optional = true, features = ["rustls"] }
|
||||
tungstenite = { workspace = true, features = ["rustls"] }
|
||||
jni = "0.21.1"
|
||||
ndk = { version = "0.9.0" }
|
||||
ndk-sys = { version = "0.6.0" }
|
||||
@@ -79,7 +81,7 @@ ndk-context = { version = "0.1.1" }
|
||||
|
||||
# use native tls on other platforms
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||
tokio-tungstenite = { workspace = true, optional = true, features = ["native-tls"] }
|
||||
tungstenite = { workspace = true, features = ["native-tls"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
cocoa = "0.26.0"
|
||||
@@ -119,7 +121,6 @@ reqwest = { workspace = true, features = ["json"] }
|
||||
http-range = { version = "0.1.5" }
|
||||
dioxus-ssr = { workspace = true, default-features = false }
|
||||
separator = "0.4.1"
|
||||
rand = { workspace = true, features = ["small_rng"] }
|
||||
|
||||
# These tests need to be run on the main thread, so they cannot use rust's test harness.
|
||||
[[test]]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
config::{Config, WindowCloseBehaviour},
|
||||
edits::EditWebsocket,
|
||||
event_handlers::WindowEventHandlers,
|
||||
file_upload::{DesktopFileUploadForm, FileDialogRequest, NativeFileEngine},
|
||||
ipc::{IpcMessage, UserWindowEvent},
|
||||
@@ -53,6 +54,7 @@ pub(crate) struct SharedContext {
|
||||
pub(crate) shortcut_manager: ShortcutRegistry,
|
||||
pub(crate) proxy: EventLoopProxy<UserWindowEvent>,
|
||||
pub(crate) target: EventLoopWindowTarget<UserWindowEvent>,
|
||||
pub(crate) websocket: EditWebsocket,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -77,6 +79,7 @@ impl App {
|
||||
shortcut_manager: ShortcutRegistry::new(),
|
||||
proxy: event_loop.create_proxy(),
|
||||
target: event_loop.clone(),
|
||||
websocket: EditWebsocket::start(),
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::path::PathBuf;
|
||||
use tao::event_loop::{EventLoop, EventLoopWindowTarget};
|
||||
use tao::window::{Icon, WindowBuilder};
|
||||
use wry::http::{Request as HttpRequest, Response as HttpResponse};
|
||||
use wry::RequestAsyncResponder;
|
||||
use wry::{RequestAsyncResponder, WebViewId};
|
||||
|
||||
use crate::ipc::UserWindowEvent;
|
||||
use crate::menubar::{default_menu_bar, DioxusMenu};
|
||||
@@ -70,12 +70,12 @@ impl LaunchConfig for Config {}
|
||||
|
||||
pub(crate) type WryProtocol = (
|
||||
String,
|
||||
Box<dyn Fn(HttpRequest<Vec<u8>>) -> HttpResponse<Cow<'static, [u8]>> + 'static>,
|
||||
Box<dyn Fn(WebViewId, HttpRequest<Vec<u8>>) -> HttpResponse<Cow<'static, [u8]>> + 'static>,
|
||||
);
|
||||
|
||||
pub(crate) type AsyncWryProtocol = (
|
||||
String,
|
||||
Box<dyn Fn(HttpRequest<Vec<u8>>, RequestAsyncResponder) + 'static>,
|
||||
Box<dyn Fn(WebViewId, HttpRequest<Vec<u8>>, RequestAsyncResponder) + 'static>,
|
||||
);
|
||||
|
||||
impl Config {
|
||||
@@ -188,7 +188,7 @@ impl Config {
|
||||
/// Set a custom protocol
|
||||
pub fn with_custom_protocol<F>(mut self, name: impl ToString, handler: F) -> Self
|
||||
where
|
||||
F: Fn(HttpRequest<Vec<u8>>) -> HttpResponse<Cow<'static, [u8]>> + 'static,
|
||||
F: Fn(WebViewId, HttpRequest<Vec<u8>>) -> HttpResponse<Cow<'static, [u8]>> + 'static,
|
||||
{
|
||||
self.protocols.push((name.to_string(), Box::new(handler)));
|
||||
self
|
||||
@@ -204,7 +204,7 @@ impl Config {
|
||||
/// #
|
||||
/// # fn main() {
|
||||
/// let cfg = Config::new()
|
||||
/// .with_asynchronous_custom_protocol("asset", |request, responder| {
|
||||
/// .with_asynchronous_custom_protocol("asset", |_webview_id, request, responder| {
|
||||
/// tokio::spawn(async move {
|
||||
/// responder.respond(
|
||||
/// HTTPResponse::builder()
|
||||
@@ -222,7 +222,7 @@ impl Config {
|
||||
/// See [`wry`](wry::WebViewBuilder::with_asynchronous_custom_protocol) for more details on implementation
|
||||
pub fn with_asynchronous_custom_protocol<F>(mut self, name: impl ToString, handler: F) -> Self
|
||||
where
|
||||
F: Fn(HttpRequest<Vec<u8>>, RequestAsyncResponder) + 'static,
|
||||
F: Fn(WebViewId, HttpRequest<Vec<u8>>, RequestAsyncResponder) + 'static,
|
||||
{
|
||||
self.asynchronous_protocols
|
||||
.push((name.to_string(), Box::new(handler)));
|
||||
|
||||
+342
-57
@@ -1,79 +1,364 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
sync::{Arc, RwLock},
|
||||
task::Waker,
|
||||
};
|
||||
//! The internal edit queue facilitating native <-> webview communication.
|
||||
//!
|
||||
//! Originally, we used long-polling on the wry custom protocol to send edits to the webview.
|
||||
//! Due to bugs in wry on android, we switched to a websocket connection that the webview connects to.
|
||||
//! We use the sledgehammer crate to build batches of edits and send them through the websocket to
|
||||
//! the webview.
|
||||
//!
|
||||
//! Using a websocket lets us send binary data to the webview quite efficiently and does encounter
|
||||
//! many of the issues with regular request/response protocols. Note that the websocket max frame
|
||||
//! size is quite large (9.22 exabytes), so we can have very large batches without issue.
|
||||
//!
|
||||
//! Using websockets does mean we need to handle security and content security policies ourselves.
|
||||
//! The code here generates a random key that the webview must use to connect to the websocket.
|
||||
//! We use the initialization script API to setup the websocket connection without leaking the key
|
||||
//! to the webview itself in case there's untrusted content in the webview.
|
||||
|
||||
use dioxus_interpreter_js::MutationState;
|
||||
use futures_channel::mpsc::UnboundedSender;
|
||||
use futures_channel::oneshot;
|
||||
use futures_util::{FutureExt, StreamExt};
|
||||
use pollster::FutureExt as _;
|
||||
use rand::{RngCore, SeedableRng};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::net::TcpListener;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::{
|
||||
net::IpAddr,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
/// This handles communication between the requests that the webview makes and the interpreter. The interpreter constantly makes long running requests to the webview to get any edits that should be made to the DOM almost like server side events.
|
||||
/// It will hold onto the requests until the interpreter is ready to handle them and hold onto any pending edits until a new request is made.
|
||||
#[derive(Default, Clone)]
|
||||
/// This handles communication between the requests that the webview makes and the interpreter.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct WryQueue {
|
||||
inner: Arc<RwLock<WryQueueInner>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct WryQueueInner {
|
||||
edit_queue: VecDeque<Vec<u8>>,
|
||||
edit_responder: Option<wry::RequestAsyncResponder>,
|
||||
// Stores any futures waiting for edits to be applied to the webview
|
||||
// NOTE: We don't use a Notify here because we need polling the notify to be cancel safe
|
||||
waiting_for_edits_flushed: Vec<Waker>,
|
||||
// If this webview is currently waiting for an edit to be flushed. We don't run the virtual dom while this is true to avoid running effects before the dom has been updated
|
||||
edits_in_progress: bool,
|
||||
mutation_state: MutationState,
|
||||
inner: Rc<RefCell<WryQueueInner>>,
|
||||
}
|
||||
|
||||
impl WryQueue {
|
||||
pub fn handle_request(&self, responder: wry::RequestAsyncResponder) {
|
||||
let mut myself = self.inner.write().unwrap();
|
||||
if let Some(bytes) = myself.edit_queue.pop_back() {
|
||||
responder.respond(wry::http::Response::new(bytes));
|
||||
} else {
|
||||
// There are now no edits that need to be applied to the webview
|
||||
for waker in myself.waiting_for_edits_flushed.drain(..) {
|
||||
waker.wake();
|
||||
}
|
||||
myself.edits_in_progress = false;
|
||||
myself.edit_responder = Some(responder);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_mutation_state_mut<O: 'static>(
|
||||
pub(crate) fn with_mutation_state_mut<O: 'static>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut MutationState) -> O,
|
||||
callback: impl FnOnce(&mut MutationState) -> O,
|
||||
) -> O {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
f(&mut inner.mutation_state)
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
callback(&mut inner.mutation_state)
|
||||
}
|
||||
|
||||
/// Send a list of mutations to the webview
|
||||
pub(crate) fn send_edits(&self) {
|
||||
let mut myself = self.inner.write().unwrap();
|
||||
let mut myself = self.inner.borrow_mut();
|
||||
let webview_id = myself.location.webview_id;
|
||||
let serialized_edits = myself.mutation_state.export_memory();
|
||||
// There are pending edits that need to be applied to the webview before we run futures
|
||||
myself.edits_in_progress = true;
|
||||
if let Some(responder) = myself.edit_responder.take() {
|
||||
responder.respond(wry::http::Response::new(serialized_edits));
|
||||
} else {
|
||||
myself.edit_queue.push_front(serialized_edits);
|
||||
}
|
||||
}
|
||||
|
||||
fn edits_in_progress(&self) -> bool {
|
||||
self.inner.read().unwrap().edits_in_progress
|
||||
let receiver = myself.websocket.send_edits(webview_id, serialized_edits);
|
||||
myself.edits_in_progress = Some(receiver);
|
||||
}
|
||||
|
||||
/// Wait until all pending edits have been rendered in the webview
|
||||
pub fn poll_edits_flushed(&self, cx: &mut std::task::Context<'_>) -> std::task::Poll<()> {
|
||||
if self.edits_in_progress() {
|
||||
let mut myself = self.inner.write().unwrap();
|
||||
let waker = cx.waker();
|
||||
myself.waiting_for_edits_flushed.push(waker.clone());
|
||||
std::task::Poll::Pending
|
||||
pub(crate) fn poll_edits_flushed(
|
||||
&self,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<()> {
|
||||
if let Some(receiver) = self.inner.borrow_mut().edits_in_progress.as_mut() {
|
||||
receiver.poll_unpin(cx).map(|_| ())
|
||||
} else {
|
||||
std::task::Poll::Ready(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the websocket path that the webview should connect to in order to receive edits
|
||||
pub(crate) fn edits_path(&self) -> String {
|
||||
let WebviewWebsocketLocation {
|
||||
port,
|
||||
webview_id,
|
||||
key,
|
||||
} = &self.inner.borrow().location;
|
||||
let key_hex = encode_key_string(key);
|
||||
format!("ws://127.0.0.1:{port}/{webview_id}/{key_hex}")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct WryQueueInner {
|
||||
location: WebviewWebsocketLocation,
|
||||
websocket: EditWebsocket,
|
||||
// If this webview is currently waiting for an edit to be flushed. We don't run the virtual dom while this is true to avoid running effects before the dom has been updated
|
||||
edits_in_progress: Option<oneshot::Receiver<()>>,
|
||||
mutation_state: MutationState,
|
||||
}
|
||||
|
||||
/// The location of a webview websocket connection. This is used to identify the webview and the port it is connected to.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct WebviewWebsocketLocation {
|
||||
/// The port the websocket is on
|
||||
port: u16,
|
||||
/// The id of the webview that this websocket is connected to
|
||||
webview_id: u32,
|
||||
/// The key that the webview will use to connect to the websocket
|
||||
key: [u8; KEY_SIZE],
|
||||
}
|
||||
|
||||
/// The websocket listener that the webview will connect to in order to receive edits and send requests. There
|
||||
/// is only one websocket listener per application even if there are multiple windows so we don't use all the
|
||||
/// open ports.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct EditWebsocket {
|
||||
/// A key that every websocket connection that originates from this application will use to identify itself.
|
||||
/// We use this to make sure no external applications can connect to our websocket and receive UI updates.
|
||||
key: [u8; KEY_SIZE],
|
||||
port: u16,
|
||||
max_webview_id: Arc<AtomicU32>,
|
||||
connections: Arc<RwLock<HashMap<u32, WebviewConnectionState>>>,
|
||||
}
|
||||
|
||||
impl EditWebsocket {
|
||||
pub(crate) fn start() -> Self {
|
||||
let expected_key = create_secure_key();
|
||||
let connections = Arc::new(RwLock::new(HashMap::new()));
|
||||
let connections_ = connections.clone();
|
||||
let server = TcpListener::bind((IpAddr::from([127, 0, 0, 1]), 0))
|
||||
.expect("Failed to bind local TCP listener for edit socket");
|
||||
let port = server.local_addr().unwrap().port();
|
||||
|
||||
std::thread::spawn(move || Self::accept_loop(server, port, expected_key, connections_));
|
||||
|
||||
Self {
|
||||
connections,
|
||||
port,
|
||||
max_webview_id: Default::default(),
|
||||
key: expected_key,
|
||||
}
|
||||
}
|
||||
|
||||
/// Accepts incoming websocket connections and handles them in a loop.
|
||||
///
|
||||
/// New sockets are accepted and then put in to a new thread to handle the connection.
|
||||
/// This is implemented using traditional sync code to allow us to be independent of the async runtime.
|
||||
fn accept_loop(
|
||||
server: TcpListener,
|
||||
port: u16,
|
||||
expected_key: [u8; KEY_SIZE],
|
||||
connections: Arc<RwLock<HashMap<u32, WebviewConnectionState>>>,
|
||||
) {
|
||||
use tungstenite::handshake::server::{Request, Response};
|
||||
|
||||
let hex_encoded_key = encode_key_string(&expected_key);
|
||||
while let Ok((stream, _)) = server.accept() {
|
||||
let mut location = None;
|
||||
|
||||
let on_request = |req: &Request, res| {
|
||||
// Try to parse the webview id and key from the path
|
||||
let path = req.uri().path();
|
||||
|
||||
// The path should have two parts `/webview_id/key`
|
||||
let mut segments = path.trim_matches('/').split('/');
|
||||
let webview_id = segments
|
||||
.next()
|
||||
.and_then(|s| s.parse::<u32>().ok())
|
||||
.ok_or_else(|| {
|
||||
Response::builder()
|
||||
.status(400)
|
||||
.body(Some("Bad Request: Invalid webview ID".to_string()))
|
||||
.unwrap()
|
||||
})?;
|
||||
let key = segments.next().ok_or_else(|| {
|
||||
Response::builder()
|
||||
.status(400)
|
||||
.body(Some("Bad Request: Missing key".to_string()))
|
||||
.unwrap()
|
||||
})?;
|
||||
|
||||
// Make sure the key matches the expected key.
|
||||
// VERY IMPORTANT: We cannot use normal string comparison here because it reveals information
|
||||
// about the key based on timing information. Instead we use a constant time comparison method.
|
||||
let key_matches: bool =
|
||||
subtle::ConstantTimeEq::ct_eq(hex_encoded_key.as_ref(), key.as_bytes()).into();
|
||||
if !key_matches {
|
||||
return Err(Response::builder()
|
||||
.status(403)
|
||||
.body(Some("Forbidden: Invalid key".to_string()))
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
location = Some(WebviewWebsocketLocation {
|
||||
port,
|
||||
webview_id,
|
||||
key: expected_key,
|
||||
});
|
||||
|
||||
Ok(res)
|
||||
};
|
||||
|
||||
// Accept the websocket connection while reading the path and setting the location
|
||||
let mut websocket = match tungstenite::accept_hdr(stream, on_request) {
|
||||
Ok(ws) => ws,
|
||||
Err(e) => {
|
||||
tracing::error!("Error accepting websocket connection: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let location = match location {
|
||||
Some(loc) => loc,
|
||||
None => {
|
||||
tracing::error!("WebSocket connection without a valid webview ID");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle the websocket connection in a separate thread
|
||||
let (edits_outgoing, mut edits_incoming_rx) =
|
||||
futures_channel::mpsc::unbounded::<MsgPair>();
|
||||
|
||||
// Spawn a task to handle the websocket connection
|
||||
std::thread::spawn(move || {
|
||||
// Wait until there are edits ready to send
|
||||
while let Some(msg) = edits_incoming_rx.next().block_on() {
|
||||
// Send the edits to the webview
|
||||
if let Err(e) = websocket.send(tungstenite::Message::Binary(msg.edits.into())) {
|
||||
tracing::error!("Error sending edits to webview: {}", e);
|
||||
break;
|
||||
}
|
||||
|
||||
// Wait for the webview to apply the edits
|
||||
while let Ok(msg) = websocket.read() {
|
||||
match msg {
|
||||
// We expect the webview to send a binary message when it has applied the edits
|
||||
// This is a signal that we can continue processing
|
||||
tungstenite::Message::Binary(_) => break,
|
||||
tungstenite::Message::Close(_) => return,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Notify that the edits have been applied
|
||||
if msg.response.send(()).is_err() {
|
||||
tracing::error!("Error sending edits applied notification");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut connections = connections.write().unwrap();
|
||||
match connections.remove(&location.webview_id) {
|
||||
// If there are pending edits, send them to the new connection
|
||||
Some(WebviewConnectionState::Pending { mut pending }) => {
|
||||
while let Some(pair) = pending.pop_front() {
|
||||
_ = edits_outgoing.unbounded_send(pair);
|
||||
}
|
||||
}
|
||||
|
||||
// If the webview was already connected, never send edits from the old connection to
|
||||
// the new connection. This should never happen
|
||||
Some(WebviewConnectionState::Connected { .. }) => {
|
||||
tracing::error!(
|
||||
"Webview {} was already connected. Rejecting new connection.",
|
||||
location.webview_id
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
None => {}
|
||||
}
|
||||
|
||||
connections.insert(
|
||||
location.webview_id,
|
||||
WebviewConnectionState::Connected { edits_outgoing },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create_queue(&self) -> WryQueue {
|
||||
WryQueue {
|
||||
inner: Rc::new(RefCell::new(WryQueueInner {
|
||||
location: WebviewWebsocketLocation {
|
||||
webview_id: self
|
||||
.max_webview_id
|
||||
.fetch_add(1, std::sync::atomic::Ordering::SeqCst),
|
||||
port: self.port,
|
||||
key: self.key,
|
||||
},
|
||||
websocket: self.clone(),
|
||||
edits_in_progress: None,
|
||||
mutation_state: MutationState::default(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
fn send_edits(&mut self, webview: u32, edits: Vec<u8>) -> oneshot::Receiver<()> {
|
||||
let mut connections_mut = self.connections.write().unwrap();
|
||||
let connection =
|
||||
connections_mut
|
||||
.entry(webview)
|
||||
.or_insert_with(|| WebviewConnectionState::Pending {
|
||||
pending: VecDeque::new(),
|
||||
});
|
||||
|
||||
match connection {
|
||||
WebviewConnectionState::Pending { pending: queue } => {
|
||||
let (response_sender, response_receiver) = oneshot::channel();
|
||||
queue.push_back(MsgPair {
|
||||
edits,
|
||||
response: response_sender,
|
||||
});
|
||||
response_receiver
|
||||
}
|
||||
WebviewConnectionState::Connected { edits_outgoing } => {
|
||||
let (response_sender, response_receiver) = oneshot::channel();
|
||||
_ = edits_outgoing.unbounded_send(MsgPair {
|
||||
edits,
|
||||
response: response_sender,
|
||||
});
|
||||
response_receiver
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The state of a webview websocket connection. This may be pending while the webview is booting.
|
||||
/// If it is, we queue up edits until the webview is ready to receive them.
|
||||
enum WebviewConnectionState {
|
||||
Pending {
|
||||
pending: VecDeque<MsgPair>,
|
||||
},
|
||||
Connected {
|
||||
edits_outgoing: UnboundedSender<MsgPair>,
|
||||
},
|
||||
}
|
||||
|
||||
struct MsgPair {
|
||||
edits: Vec<u8>,
|
||||
response: oneshot::Sender<()>,
|
||||
}
|
||||
|
||||
const KEY_SIZE: usize = 256;
|
||||
type EncodedKey = [u8; KEY_SIZE];
|
||||
|
||||
/// Base64 encode the key to a string to be used in the websocket URL.
|
||||
fn encode_key_string(key: &EncodedKey) -> String {
|
||||
base64::Engine::encode(&base64::engine::general_purpose::URL_SAFE, key)
|
||||
}
|
||||
|
||||
/// Create a secure key for the websocket connection.
|
||||
/// Returns the key as a byte array and a hex-encoded string representation of the key.
|
||||
fn create_secure_key() -> EncodedKey {
|
||||
// Helper function to assert that the RNG is a CryptoRng - make sure we use a secure RNG
|
||||
fn assert_crypto_random<R: rand::CryptoRng>(val: R) -> R {
|
||||
val
|
||||
}
|
||||
|
||||
let mut secure_rng = assert_crypto_random(rand::rngs::StdRng::from_entropy());
|
||||
let mut expected_key: EncodedKey = [0u8; KEY_SIZE];
|
||||
secure_rng.fill_bytes(&mut expected_key);
|
||||
expected_key
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_key_encoding_length() {
|
||||
let mut rand = rand::rngs::StdRng::from_entropy();
|
||||
for _ in 0..100 {
|
||||
let mut key: EncodedKey = [0u8; KEY_SIZE];
|
||||
rand.fill_bytes(&mut key);
|
||||
let encoded = encode_key_string(&key);
|
||||
// The encoded key length should be the same regardless of the value of the key
|
||||
assert_eq!(encoded.len(), 344);
|
||||
}
|
||||
}
|
||||
@@ -1,228 +1,3 @@
|
||||
// use crate::Config;
|
||||
// use crate::{
|
||||
// app::App,
|
||||
// ipc::{IpcMethod, UserWindowEvent},
|
||||
// };
|
||||
// use dioxus_core::*;
|
||||
// use dioxus_document::eval;
|
||||
// use std::any::Any;
|
||||
// use tao::event::{Event, StartCause, WindowEvent};
|
||||
|
||||
// /// Launches the WebView and runs the event loop
|
||||
// pub fn launch(root: fn() -> Element) {
|
||||
// launch_cfg(root, vec![], vec![]);
|
||||
// }
|
||||
|
||||
// /// Launches the WebView and runs the event loop, with configuration and root props.
|
||||
// pub fn launch_cfg(
|
||||
// root: fn() -> Element,
|
||||
// contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
|
||||
// platform_config: Vec<Box<dyn Any>>,
|
||||
// ) {
|
||||
// let mut virtual_dom = VirtualDom::new(root);
|
||||
|
||||
// for context in contexts {
|
||||
// virtual_dom.insert_any_root_context(context());
|
||||
// }
|
||||
|
||||
// let platform_config = *platform_config
|
||||
// .into_iter()
|
||||
// .find_map(|cfg| cfg.downcast::<Config>().ok())
|
||||
// .unwrap_or_default();
|
||||
|
||||
// launch_virtual_dom(virtual_dom, platform_config)
|
||||
// }
|
||||
|
||||
// /// Launches the WebView and runs the event loop, with configuration and root props.
|
||||
// pub fn launch_virtual_dom(virtual_dom: VirtualDom, desktop_config: Config) -> ! {
|
||||
// #[cfg(feature = "tokio_runtime")]
|
||||
// {
|
||||
// tokio::runtime::Builder::new_multi_thread()
|
||||
// .enable_all()
|
||||
// .build()
|
||||
// .unwrap()
|
||||
// .block_on(tokio::task::unconstrained(async move {
|
||||
// launch_virtual_dom_blocking(virtual_dom, desktop_config)
|
||||
// }));
|
||||
|
||||
// unreachable!("The desktop launch function will never exit")
|
||||
// }
|
||||
|
||||
// #[cfg(not(feature = "tokio_runtime"))]
|
||||
// {
|
||||
// launch_virtual_dom_blocking(virtual_dom, desktop_config);
|
||||
// }
|
||||
// }
|
||||
|
||||
// /// Launch the WebView and run the event loop, with configuration and root props.
|
||||
// ///
|
||||
// /// This will block the main thread, and *must* be spawned on the main thread. This function does not assume any runtime
|
||||
// /// and is equivalent to calling launch_with_props with the tokio feature disabled.
|
||||
// pub fn launch_virtual_dom_blocking(virtual_dom: VirtualDom, mut desktop_config: Config) -> ! {
|
||||
// let mut custom_event_handler = desktop_config.custom_event_handler.take();
|
||||
// let (event_loop, mut app) = App::new(desktop_config, virtual_dom);
|
||||
|
||||
// event_loop.run(move |window_event, event_loop, control_flow| {
|
||||
// // Set the control flow and check if any events need to be handled in the app itself
|
||||
// app.tick(&window_event);
|
||||
|
||||
// if let Some(ref mut f) = custom_event_handler {
|
||||
// f(&window_event, event_loop)
|
||||
// }
|
||||
|
||||
// match window_event {
|
||||
// Event::NewEvents(StartCause::Init) => app.handle_start_cause_init(),
|
||||
// Event::LoopDestroyed => app.handle_loop_destroyed(),
|
||||
// Event::WindowEvent {
|
||||
// event, window_id, ..
|
||||
// } => match event {
|
||||
// WindowEvent::CloseRequested => app.handle_close_requested(window_id),
|
||||
// WindowEvent::Destroyed { .. } => app.window_destroyed(window_id),
|
||||
// WindowEvent::Resized(new_size) => app.resize_window(window_id, new_size),
|
||||
// _ => {}
|
||||
// },
|
||||
|
||||
// Event::UserEvent(event) => match event {
|
||||
// UserWindowEvent::Poll(id) => app.poll_vdom(id),
|
||||
// UserWindowEvent::NewWindow => app.handle_new_window(),
|
||||
// UserWindowEvent::CloseWindow(id) => app.handle_close_msg(id),
|
||||
// UserWindowEvent::Shutdown => app.control_flow = tao::event_loop::ControlFlow::Exit,
|
||||
|
||||
// #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
// UserWindowEvent::GlobalHotKeyEvent(evnt) => app.handle_global_hotkey(evnt),
|
||||
|
||||
// #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
// UserWindowEvent::MudaMenuEvent(evnt) => app.handle_menu_event(evnt),
|
||||
|
||||
// #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
// UserWindowEvent::TrayMenuEvent(evnt) => app.handle_tray_menu_event(evnt),
|
||||
|
||||
// #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
// UserWindowEvent::TrayIconEvent(evnt) => app.handle_tray_icon_event(evnt),
|
||||
|
||||
// #[cfg(all(feature = "devtools", debug_assertions))]
|
||||
// UserWindowEvent::HotReloadEvent(msg) => app.handle_hot_reload_msg(msg),
|
||||
|
||||
// // Windows-only drag-n-drop fix events. We need to call the interpreter drag-n-drop code.
|
||||
// UserWindowEvent::WindowsDragDrop(id) => {
|
||||
// if let Some(webview) = app.webviews.get(&id) {
|
||||
// webview.dom.in_runtime(|| {
|
||||
// ScopeId::ROOT.in_runtime(|| {
|
||||
// eval("window.interpreter.handleWindowsDragDrop();");
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// UserWindowEvent::WindowsDragLeave(id) => {
|
||||
// if let Some(webview) = app.webviews.get(&id) {
|
||||
// webview.dom.in_runtime(|| {
|
||||
// ScopeId::ROOT.in_runtime(|| {
|
||||
// eval("window.interpreter.handleWindowsDragLeave();");
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// UserWindowEvent::WindowsDragOver(id, x_pos, y_pos) => {
|
||||
// if let Some(webview) = app.webviews.get(&id) {
|
||||
// webview.dom.in_runtime(|| {
|
||||
// ScopeId::ROOT.in_runtime(|| {
|
||||
// let e = eval(
|
||||
// r#"
|
||||
// const xPos = await dioxus.recv();
|
||||
// const yPos = await dioxus.recv();
|
||||
// window.interpreter.handleWindowsDragOver(xPos, yPos)
|
||||
// "#,
|
||||
// );
|
||||
|
||||
// _ = e.send(x_pos);
|
||||
// _ = e.send(y_pos);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// UserWindowEvent::Ipc { id, msg } => match msg.method() {
|
||||
// IpcMethod::Initialize => app.handle_initialize_msg(id),
|
||||
// IpcMethod::FileDialog => app.handle_file_dialog_msg(msg, id),
|
||||
// IpcMethod::UserEvent => {}
|
||||
// IpcMethod::Query => app.handle_query_msg(msg, id),
|
||||
// IpcMethod::BrowserOpen => app.handle_browser_open(msg),
|
||||
// IpcMethod::Other(_) => {}
|
||||
// },
|
||||
// },
|
||||
// _ => {}
|
||||
// }
|
||||
|
||||
// *control_flow = app.control_flow;
|
||||
// })
|
||||
// }
|
||||
|
||||
// /// Expose the `Java_dev_dioxus_main_WryActivity_create` function to the JNI layer.
|
||||
// /// We hardcode these to have a single trampoline for host Java code to call into.
|
||||
// ///
|
||||
// /// This saves us from having to plumb the top-level package name all the way down into
|
||||
// /// this file. This is better for modularity (ie just call dioxus' main to run the app) as
|
||||
// /// well as cache thrashing since this crate doesn't rely on external env vars.
|
||||
// ///
|
||||
// /// The CLI is expecting to find `dev.dioxus.main` in the final library. If you find a need to
|
||||
// /// change this, you'll need to change the CLI as well.
|
||||
// #[cfg(target_os = "android")]
|
||||
// #[no_mangle]
|
||||
// #[inline(never)]
|
||||
// pub extern "C" fn start_app() {
|
||||
// wry::android_binding!(dev_dioxus, main, wry);
|
||||
// tao::android_binding!(
|
||||
// dev_dioxus,
|
||||
// main,
|
||||
// WryActivity,
|
||||
// wry::android_setup,
|
||||
// dioxus_main_root_fn,
|
||||
// tao
|
||||
// );
|
||||
// }
|
||||
|
||||
// #[cfg(target_os = "android")]
|
||||
// #[doc(hidden)]
|
||||
// pub fn dioxus_main_root_fn() {
|
||||
// // we're going to find the `main` symbol using dlsym directly and call it
|
||||
// unsafe {
|
||||
// let mut main_fn_ptr = libc::dlsym(libc::RTLD_DEFAULT, b"main\0".as_ptr() as _);
|
||||
|
||||
// if main_fn_ptr.is_null() {
|
||||
// main_fn_ptr = libc::dlsym(libc::RTLD_DEFAULT, b"_main\0".as_ptr() as _);
|
||||
// }
|
||||
|
||||
// if main_fn_ptr.is_null() {
|
||||
// panic!("Failed to find main symbol");
|
||||
// }
|
||||
|
||||
// // Set the env vars that rust code might expect, passed off to us by the android app
|
||||
// // Doing this before main emulates the behavior of a regular executable
|
||||
// if cfg!(target_os = "android") && cfg!(debug_assertions) {
|
||||
// load_env_file_from_session_cache();
|
||||
// }
|
||||
|
||||
// let main_fn: extern "C" fn() = std::mem::transmute(main_fn_ptr);
|
||||
// main_fn();
|
||||
// };
|
||||
// }
|
||||
|
||||
// /// Load the env file from the session cache if we're in debug mode and on android
|
||||
// ///
|
||||
// /// This is a slightly hacky way of being able to use std::env::var code in android apps without
|
||||
// /// going through their custom java-based system.
|
||||
// #[cfg(target_os = "android")]
|
||||
// fn load_env_file_from_session_cache() {
|
||||
// let env_file = dioxus_cli_config::android_session_cache_dir().join(".env");
|
||||
// if let Some(env_file) = std::fs::read_to_string(&env_file).ok() {
|
||||
// for line in env_file.lines() {
|
||||
// if let Some((key, value)) = line.trim().split_once('=') {
|
||||
// std::env::set_var(key, value);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
use crate::Config;
|
||||
use crate::{
|
||||
app::App,
|
||||
|
||||
@@ -7,12 +7,6 @@ use wry::{
|
||||
RequestAsyncResponder,
|
||||
};
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "windows"))]
|
||||
const EDITS_PATH: &str = "http://dioxus.index.html/__edits";
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "windows")))]
|
||||
const EDITS_PATH: &str = "dioxus://index.html/__edits";
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "windows"))]
|
||||
const EVENTS_PATH: &str = "http://dioxus.index.html/__events";
|
||||
|
||||
@@ -42,17 +36,19 @@ pub(super) fn desktop_handler(
|
||||
headless: bool,
|
||||
) {
|
||||
// Try to serve the index file first
|
||||
if let Some(index_bytes) =
|
||||
index_request(&request, custom_head, custom_index, root_name, headless)
|
||||
{
|
||||
if let Some(index_bytes) = index_request(
|
||||
&request,
|
||||
custom_head,
|
||||
custom_index,
|
||||
root_name,
|
||||
headless,
|
||||
edit_state,
|
||||
) {
|
||||
return responder.respond(index_bytes);
|
||||
}
|
||||
|
||||
// If the request is asking for edits (ie binary protocol streaming), do that
|
||||
let trimmed_uri = request.uri().path().trim_matches('/');
|
||||
if trimmed_uri == "__edits" {
|
||||
return edit_state.wry_queue.handle_request(responder);
|
||||
}
|
||||
|
||||
// If the request is asking for an event response, do that
|
||||
if trimmed_uri == "__events" {
|
||||
@@ -93,6 +89,7 @@ fn index_request(
|
||||
custom_index: Option<String>,
|
||||
root_name: &str,
|
||||
headless: bool,
|
||||
edit_state: &WebviewEdits,
|
||||
) -> Option<Response<Vec<u8>>> {
|
||||
// If the request is for the root, we'll serve the index.html file.
|
||||
if request.uri().path() != "/" {
|
||||
@@ -114,7 +111,7 @@ fn index_request(
|
||||
// Might want to document this
|
||||
index.insert_str(
|
||||
index.find("</body>").expect("Body element to exist"),
|
||||
&module_loader(root_name, headless),
|
||||
&module_loader(root_name, headless, edit_state),
|
||||
);
|
||||
|
||||
Response::builder()
|
||||
@@ -130,7 +127,12 @@ fn index_request(
|
||||
/// - root_name: the root element (by Id) that we stream edits into
|
||||
/// - headless: is this page being loaded but invisible? Important because not all windows are visible and the
|
||||
/// interpreter can't connect until the window is ready.
|
||||
fn module_loader(root_id: &str, headless: bool) -> String {
|
||||
/// - port: the port that the websocket server is listening on for edits
|
||||
/// - webview_id: the id of the webview that we're loading this into. This is used to differentiate between
|
||||
/// multiple webviews in the same application, so that we can send edits to the correct one.
|
||||
fn module_loader(root_id: &str, headless: bool, edit_state: &WebviewEdits) -> String {
|
||||
let edits_path = edit_state.wry_queue.edits_path();
|
||||
|
||||
format!(
|
||||
r#"
|
||||
<script type="module">
|
||||
@@ -141,7 +143,7 @@ fn module_loader(root_id: &str, headless: bool) -> String {
|
||||
{NATIVE_JS}
|
||||
|
||||
// The native interpreter extends the sledgehammer interpreter with a few extra methods that we use for IPC
|
||||
window.interpreter = new NativeInterpreter("{EDITS_PATH}", "{EVENTS_PATH}");
|
||||
window.interpreter = new NativeInterpreter("{edits_path}", "{EVENTS_PATH}");
|
||||
|
||||
// Wait for the page to load before sending the initialize message
|
||||
window.onload = function() {{
|
||||
|
||||
@@ -23,7 +23,7 @@ use futures_util::{pin_mut, FutureExt};
|
||||
use std::sync::Arc;
|
||||
use std::{cell::OnceCell, time::Duration};
|
||||
use std::{rc::Rc, task::Waker};
|
||||
use wry::{DragDropEvent, RequestAsyncResponder, WebContext, WebViewBuilder};
|
||||
use wry::{DragDropEvent, RequestAsyncResponder, WebContext, WebViewBuilder, WebViewId};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct WebviewEdits {
|
||||
@@ -223,7 +223,7 @@ impl WebviewInstance {
|
||||
}
|
||||
|
||||
let mut web_context = WebContext::new(cfg.data_dir.clone());
|
||||
let edit_queue = WryQueue::default();
|
||||
let edit_queue = shared.websocket.create_queue();
|
||||
let asset_handlers = AssetHandlerRegistry::new();
|
||||
let edits = WebviewEdits::new(dom.runtime(), edit_queue.clone());
|
||||
let file_hover = NativeFileHover::default();
|
||||
@@ -237,7 +237,7 @@ impl WebviewInstance {
|
||||
asset_handlers,
|
||||
edits
|
||||
];
|
||||
move |request, responder: RequestAsyncResponder| {
|
||||
move |_id: WebViewId, request, responder: RequestAsyncResponder| {
|
||||
protocol::desktop_handler(
|
||||
request,
|
||||
asset_handlers.clone(),
|
||||
@@ -265,23 +265,17 @@ impl WebviewInstance {
|
||||
|
||||
let file_drop_handler = {
|
||||
to_owned![file_hover];
|
||||
|
||||
#[cfg(windows)]
|
||||
let (proxy, window_id) = (shared.proxy.to_owned(), window.id());
|
||||
|
||||
move |evt: DragDropEvent| {
|
||||
// Update the most recent file drop event - when the event comes in from the webview we can use the
|
||||
// most recent event to build a new event with the files in it.
|
||||
#[cfg(not(windows))]
|
||||
file_hover.set(evt);
|
||||
|
||||
// Windows webview blocks HTML-native events when the drop handler is provided.
|
||||
// The problem is that the HTML-native events don't provide the file, so we need this.
|
||||
// Solution: this glue code to mimic drag drop events.
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if cfg!(not(windows)) {
|
||||
// Update the most recent file drop event - when the event comes in from the webview we can use the
|
||||
// most recent event to build a new event with the files in it.
|
||||
file_hover.set(evt);
|
||||
} else {
|
||||
// Windows webview blocks HTML-native events when the drop handler is provided.
|
||||
// The problem is that the HTML-native events don't provide the file, so we need this.
|
||||
// Solution: this glue code to mimic drag drop events.
|
||||
file_hover.set(evt.clone());
|
||||
|
||||
match evt {
|
||||
wry::DragDropEvent::Drop {
|
||||
paths: _,
|
||||
@@ -305,33 +299,7 @@ impl WebviewInstance {
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
target_os = "macos",
|
||||
target_os = "ios",
|
||||
target_os = "android"
|
||||
))]
|
||||
let mut webview = if cfg.as_child_window {
|
||||
WebViewBuilder::new_as_child(&window)
|
||||
} else {
|
||||
WebViewBuilder::new(&window)
|
||||
};
|
||||
|
||||
#[cfg(not(any(
|
||||
target_os = "windows",
|
||||
target_os = "macos",
|
||||
target_os = "ios",
|
||||
target_os = "android"
|
||||
)))]
|
||||
let mut webview = {
|
||||
use tao::platform::unix::WindowExtUnix;
|
||||
use wry::WebViewBuilderExtUnix;
|
||||
let vbox = window.default_vbox().unwrap();
|
||||
WebViewBuilder::new_gtk(vbox)
|
||||
};
|
||||
|
||||
webview = webview
|
||||
.with_web_context(&mut web_context)
|
||||
let mut webview = WebViewBuilder::new_with_web_context(&mut web_context)
|
||||
.with_bounds(wry::Rect {
|
||||
position: wry::dpi::Position::Logical(wry::dpi::LogicalPosition::new(0.0, 0.0)),
|
||||
size: wry::dpi::Size::Physical(wry::dpi::PhysicalSize::new(
|
||||
@@ -412,7 +380,32 @@ impl WebviewInstance {
|
||||
None
|
||||
};
|
||||
|
||||
let webview = webview.build().unwrap();
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
target_os = "macos",
|
||||
target_os = "ios",
|
||||
target_os = "android"
|
||||
))]
|
||||
let webview = if cfg.as_child_window {
|
||||
webview.build_as_child(&window)
|
||||
} else {
|
||||
webview.build(&window)
|
||||
};
|
||||
|
||||
#[cfg(not(any(
|
||||
target_os = "windows",
|
||||
target_os = "macos",
|
||||
target_os = "ios",
|
||||
target_os = "android"
|
||||
)))]
|
||||
let webview = {
|
||||
use tao::platform::unix::WindowExtUnix;
|
||||
use wry::WebViewBuilderExtUnix;
|
||||
let vbox = window.default_vbox().unwrap();
|
||||
webview.build_gtk(vbox)
|
||||
};
|
||||
let webview = webview.unwrap();
|
||||
|
||||
let desktop_context = Rc::from(DesktopService::new(
|
||||
webview,
|
||||
window,
|
||||
|
||||
@@ -1 +1 @@
|
||||
[6449103750905854967, 17669692872757955279, 13069001215487072322, 11420464406527728232, 3770103091118609057, 5444526391971481782, 14470003670721083879, 5052021921702764563, 10988859153374944111, 11339769846046015954]
|
||||
[6449103750905854967, 17669692872757955279, 13069001215487072322, 11420464406527728232, 3770103091118609057, 5444526391971481782, 9821096297440533852, 5052021921702764563, 10988859153374944111, 11339769846046015954]
|
||||
@@ -1 +1 @@
|
||||
function retrieveValues(event,target){let contents={values:{}},form=target.closest("form");if(form){if(event.type==="input"||event.type==="change"||event.type==="submit"||event.type==="reset"||event.type==="click")contents=retrieveFormValues(form)}return contents}function retrieveFormValues(form){let formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(contents[key])contents[key].push(value);else contents[key]=[value]}),{valid:form.checkValidity(),values:contents}}function retrieveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;i<options.length;i++)values.push(options[i].value);return values}function serializeEvent(event,target){let contents={},extend=(obj)=>contents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(event instanceof CustomEvent){let detail=event.detail;if(detail instanceof ResizeObserverEntry)extend(serializeResizeEventDetail(detail));else if(detail instanceof IntersectionObserverEntry)extend(serializeIntersectionEventDetail(detail))}if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);if(event.type==="scroll")extend(serializeScrollEvent(event));return contents}function toSerializableResizeObserverSize(size,is_inline_width){return[is_inline_width?size.inlineSize:size.blockSize,is_inline_width?size.blockSize:size.inlineSize]}function serializeResizeEventDetail(detail){let is_inline_width=!0;if(detail.target instanceof HTMLElement){if(window.getComputedStyle(detail.target).getPropertyValue("writing-mode")!=="horizontal-tb")is_inline_width=!1}return{border_box_size:detail.borderBoxSize!==void 0?toSerializableResizeObserverSize(detail.borderBoxSize[0],is_inline_width):detail.contentRect,content_box_size:detail.contentBoxSize!==void 0?toSerializableResizeObserverSize(detail.contentBoxSize[0],is_inline_width):detail.contentRect,content_rect:detail.contentRect}}function serializeIntersectionEventDetail(detail){return{bounding_client_rect:detail.boundingClientRect,intersection_ratio:detail.intersectionRatio,intersection_rect:detail.intersectionRect,is_intersecting:detail.isIntersecting,root_bounds:detail.rootBounds,time_ms:Math.floor(Date.now()+detail.time)}}function serializeInputEvent(event,target){let contents={};if(target instanceof HTMLElement){let values=retrieveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retrieveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents}function serializeWheelEvent(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}}function serializeTouchEvent(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:event.changedTouches,target_touches:event.targetTouches,touches:event.touches}}function serializePointerEvent(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}}function serializeMouseEvent(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,offset_x:event.offsetX,offset_y:event.ofLine truncated
|
||||
function retrieveValues(event,target){let contents={values:{}},form=target.closest("form");if(form){if(event.type==="input"||event.type==="change"||event.type==="submit"||event.type==="reset"||event.type==="click")contents=retrieveFormValues(form)}return contents}function retrieveFormValues(form){let formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(contents[key])contents[key].push(value);else contents[key]=[value]}),{valid:form.checkValidity(),values:contents}}function retrieveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;i<options.length;i++)values.push(options[i].value);return values}function serializeEvent(event,target){let contents={},extend=(obj)=>contents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(event instanceof CustomEvent){let detail=event.detail;if(detail instanceof ResizeObserverEntry)extend(serializeResizeEventDetail(detail));else if(detail instanceof IntersectionObserverEntry)extend(serializeIntersectionEventDetail(detail))}if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);if(event.type==="scroll")extend(serializeScrollEvent(event));return contents}function toSerializableResizeObserverSize(size,is_inline_width){return[is_inline_width?size.inlineSize:size.blockSize,is_inline_width?size.blockSize:size.inlineSize]}function serializeResizeEventDetail(detail){let is_inline_width=!0;if(detail.target instanceof HTMLElement){if(window.getComputedStyle(detail.target).getPropertyValue("writing-mode")!=="horizontal-tb")is_inline_width=!1}return{border_box_size:detail.borderBoxSize!==void 0?toSerializableResizeObserverSize(detail.borderBoxSize[0],is_inline_width):detail.contentRect,content_box_size:detail.contentBoxSize!==void 0?toSerializableResizeObserverSize(detail.contentBoxSize[0],is_inline_width):detail.contentRect,content_rect:detail.contentRect}}function serializeIntersectionEventDetail(detail){return{bounding_client_rect:detail.boundingClientRect,intersection_ratio:detail.intersectionRatio,intersection_rect:detail.intersectionRect,is_intersecting:detail.isIntersecting,root_bounds:detail.rootBounds,time_ms:Math.floor(Date.now()+detail.time)}}function serializeInputEvent(event,target){let contents={};if(target instanceof HTMLElement){let values=retrieveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retrieveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents}function serializeWheelEvent(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}}function serializeTouchEvent(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:event.changedTouches,target_touches:event.targetTouches,touches:event.touches}}function serializePointerEvent(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}}function serializeMouseEvent(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,offset_x:event.offsetX,offset_y:event.ofLine truncated
|
||||
@@ -20,7 +20,7 @@ if (RawInterpreter !== undefined && RawInterpreter !== null) {
|
||||
export class NativeInterpreter extends JSChannel_ {
|
||||
intercept_link_redirects: boolean;
|
||||
ipc: any;
|
||||
editsPath: string;
|
||||
edits: WebSocket;
|
||||
eventsPath: string;
|
||||
kickStylesheets: boolean;
|
||||
queuedBytes: ArrayBuffer[] = [];
|
||||
@@ -31,7 +31,7 @@ export class NativeInterpreter extends JSChannel_ {
|
||||
|
||||
constructor(editsPath: string, eventsPath: string) {
|
||||
super();
|
||||
this.editsPath = editsPath;
|
||||
this.edits = new WebSocket(editsPath);
|
||||
this.eventsPath = eventsPath;
|
||||
this.kickStylesheets = false;
|
||||
}
|
||||
@@ -373,23 +373,32 @@ export class NativeInterpreter extends JSChannel_ {
|
||||
if (headless) {
|
||||
// @ts-ignore
|
||||
this.run_from_bytes(bytes);
|
||||
this.waitForRequest(headless);
|
||||
this.markEditsFinished();
|
||||
} else {
|
||||
this.enqueueBytes(bytes);
|
||||
requestAnimationFrame(() => {
|
||||
this.flushQueuedBytes();
|
||||
// With request animation frames, we use the next reqwest as a marker to know when the frame is done and it is safe to run effects
|
||||
this.waitForRequest(headless);
|
||||
this.markEditsFinished();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
waitForRequest(headless: boolean) {
|
||||
fetch(new Request(this.editsPath))
|
||||
.then((response) => response.arrayBuffer())
|
||||
.then((bytes) => {
|
||||
this.rafEdits(headless, bytes);
|
||||
});
|
||||
this.edits.onmessage = (event) => {
|
||||
const data = event.data;
|
||||
if (data instanceof Blob) {
|
||||
// If the data is a blob, we need to convert it to an ArrayBuffer
|
||||
data.arrayBuffer().then((buffer) => {
|
||||
this.rafEdits(headless, buffer);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
markEditsFinished() {
|
||||
// Send an empty ArrayBuffer to the edits websocket to signal that the edits are finished
|
||||
// This is used to signal that the edits are done and the next request can be processed
|
||||
this.edits.send(new ArrayBuffer(0));
|
||||
}
|
||||
|
||||
kickAllStylesheetsOnPage() {
|
||||
|
||||
Reference in new issue
Block a user