use crate::file_upload::{DesktopFileData, DesktopFileDragEvent};
use crate::menubar::DioxusMenu;
use crate::PendingDesktopContext;
use crate::{
app::SharedContext, assets::AssetHandlerRegistry, edits::WryQueue,
file_upload::NativeFileHover, ipc::UserWindowEvent, protocol, waker::tao_waker, Config,
DesktopContext, DesktopService,
};
use crate::{document::DesktopDocument, WeakDesktopContext};
use crate::{element::DesktopElement, file_upload::DesktopFormData};
use base64::prelude::BASE64_STANDARD;
use dioxus_core::{consume_context, provide_context, Runtime, ScopeId, VirtualDom};
use dioxus_document::Document;
use dioxus_history::{History, MemoryHistory};
use dioxus_hooks::to_owned;
use dioxus_html::{FileData, FormValue, HtmlEvent, PlatformEventData, SerializedFileData};
use futures_util::{pin_mut, FutureExt};
use std::sync::{atomic::AtomicBool, Arc};
use std::{cell::OnceCell, time::Duration};
use std::{rc::Rc, task::Waker};
use wry::{DragDropEvent, RequestAsyncResponder, WebContext, WebViewBuilder, WebViewId};
#[derive(Clone)]
pub(crate) struct WebviewEdits {
runtime: Rc<Runtime>,
pub wry_queue: WryQueue,
desktop_context: Rc<OnceCell<WeakDesktopContext>>,
}
impl WebviewEdits {
fn new(runtime: Rc<Runtime>, wry_queue: WryQueue) -> Self {
Self {
runtime,
wry_queue,
desktop_context: Default::default(),
}
}
fn set_desktop_context(&self, context: WeakDesktopContext) {
_ = self.desktop_context.set(context);
}
pub fn handle_event(
&self,
request: wry::http::Request<Vec<u8>>,
responder: wry::RequestAsyncResponder,
) {
let body = self
.try_handle_event(request)
.expect("Writing bodies to succeed");
responder.respond(wry::http::Response::new(body))
}
pub fn try_handle_event(
&self,
request: wry::http::Request<Vec<u8>>,
) -> Result<Vec<u8>, serde_json::Error> {
use serde::de::Error;
let data = request
.headers()
.get("dioxus-data")
.ok_or_else(|| Error::custom("dioxus-data header not set"))?;
let as_utf = std::str::from_utf8(data.as_bytes())
.map_err(|_| Error::custom("dioxus-data header is not a valid (utf-8) string"))?;
let data_from_header = base64::Engine::decode(&BASE64_STANDARD, as_utf)
.map_err(|_| Error::custom("dioxus-data header is not a base64 string"))?;
let response = match serde_json::from_slice(&data_from_header) {
Ok(event) => {
#[cfg(target_os = "android")]
let _lock = crate::android_sync_lock::android_runtime_lock();
self.handle_html_event(event)
}
Err(err) => {
tracing::error!(
"Error parsing user_event: {:?}. \n Contents: {:?}, \nraw: {:#?}",
err,
String::from_utf8(request.body().to_vec()),
request
);
SynchronousEventResponse::new(false)
}
};
serde_json::to_vec(&response).inspect_err(|err| {
tracing::error!("failed to serialize SynchronousEventResponse: {err:?}");
})
}
pub fn handle_html_event(&self, event: HtmlEvent) -> SynchronousEventResponse {
let HtmlEvent {
element,
name,
bubbles,
data,
} = event;
let Some(desktop_context) = self.desktop_context.get() else {
tracing::error!(
"Tried to handle event before setting the desktop context on the event handler"
);
return Default::default();
};
let desktop_context = desktop_context.upgrade().unwrap();
let query = desktop_context.query.clone();
let hovered_file = desktop_context.file_hover.clone();
let as_any = match data {
dioxus_html::EventData::Mounted => {
let element = DesktopElement::new(element, desktop_context.clone(), query.clone());
Rc::new(PlatformEventData::new(Box::new(element)))
}
dioxus_html::EventData::Form(form) => {
Rc::new(PlatformEventData::new(Box::new(DesktopFormData {
value: form.value,
valid: form.valid,
values: form
.values
.into_iter()
.map(|obj| {
if let Some(text) = obj.text {
return (obj.key, FormValue::Text(text));
}
if let Some(file_data) = obj.file {
if file_data.path.capacity() == 0 {
return (obj.key, FormValue::File(None));
}
return (
obj.key,
FormValue::File(Some(FileData::new(DesktopFileData(
file_data.path,
)))),
);
};
(obj.key, FormValue::Text(String::new()))
})
.collect(),
})))
}
dioxus_html::EventData::Drag(ref drag) => {
let full_file_paths = hovered_file.current_paths();
let xfer_data = drag.data_transfer.clone();
let new_file_data = xfer_data
.files
.iter()
.map(|f| {
let new_path = full_file_paths
.iter()
.find(|p| p.ends_with(&f.path))
.unwrap_or(&f.path);
SerializedFileData {
path: new_path.clone(),
..f.clone()
}
})
.collect::<Vec<_>>();
let new_xfer_data = dioxus_html::SerializedDataTransfer {
files: new_file_data,
..xfer_data
};
Rc::new(PlatformEventData::new(Box::new(DesktopFileDragEvent {
mouse: drag.mouse.clone(),
data_transfer: new_xfer_data,
files: full_file_paths,
})))
}
_ => data.into_any(),
};
let event = dioxus_core::Event::new(as_any, bubbles);
self.runtime.handle_event(&name, event.clone(), element);
SynchronousEventResponse::new(!event.default_action_enabled())
}
}
pub(crate) struct WebviewInstance {
pub dom: VirtualDom,
pub edits: WebviewEdits,
pub desktop_context: DesktopContext,
pub waker: Waker,
_web_context: WebContext,
_menu: Option<DioxusMenu>,
}
impl WebviewInstance {
pub(crate) fn new(
mut cfg: Config,
mut dom: VirtualDom,
shared: Rc<SharedContext>,
) -> WebviewInstance {
let mut window = cfg.window.clone();
#[cfg(not(any(target_os = "ios", target_os = "android")))]
{
if cfg.window.window.inner_size.is_none() {
window = window.with_inner_size(tao::dpi::LogicalSize::new(800.0, 600.0));
}
}
if cfg.window.window.window_icon.is_none() {
window = window.with_window_icon(Some(
tao::window::Icon::from_rgba(
include_bytes!("./assets/default_icon.bin").to_vec(),
460,
460,
)
.expect("image parse failed"),
));
}
let window = Arc::new(window.build(&shared.target).unwrap());
if let Some(on_build) = cfg.on_window.as_mut() {
on_build(window.clone(), &mut dom);
}
#[cfg(target_os = "macos")]
#[allow(deprecated)]
{
use cocoa::appkit::NSWindowCollectionBehavior;
use cocoa::base::id;
use objc::{msg_send, sel, sel_impl};
use tao::platform::macos::WindowExtMacOS;
unsafe {
let window: id = window.ns_window() as id;
let _: () = msg_send![window, setCollectionBehavior: NSWindowCollectionBehavior::NSWindowCollectionBehaviorManaged];
}
}
let mut web_context = WebContext::new(cfg.data_dir.clone().or_else(|| {
if cfg!(windows) {
let exe = std::env::current_exe().ok()?;
let name = exe.file_stem()?.to_str()?;
let local_app_data = std::env::var("LOCALAPPDATA").ok()?;
Some(std::path::PathBuf::from(local_app_data).join(name))
} else {
None
}
}));
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();
let headless = !cfg.window.window.visible;
let request_handler = {
to_owned![
cfg.custom_head,
cfg.custom_index,
cfg.root_name,
asset_handlers,
edits
];
#[cfg(feature = "tokio_runtime")]
let tokio_rt = tokio::runtime::Handle::current();
move |_id: WebViewId, request, responder: RequestAsyncResponder| {
#[cfg(feature = "tokio_runtime")]
let _guard = tokio_rt.enter();
protocol::desktop_handler(
request,
asset_handlers.clone(),
responder,
&edits,
custom_head.clone(),
custom_index.clone(),
&root_name,
headless,
)
}
};
let ipc_handler = {
let window_id = window.id();
to_owned![shared.proxy];
move |payload: wry::http::Request<String>| {
let body = payload.into_body();
if let Ok(msg) = serde_json::from_str(&body) {
_ = proxy.send_event(UserWindowEvent::Ipc { id: window_id, msg });
}
}
};
let file_drop_handler = {
to_owned![file_hover];
let (proxy, window_id) = (shared.proxy.to_owned(), window.id());
move |evt: DragDropEvent| {
if cfg!(not(windows)) {
file_hover.set(evt);
} else {
file_hover.set(evt.clone());
match evt {
wry::DragDropEvent::Drop {
paths: _,
position: _,
} => {
_ = proxy.send_event(UserWindowEvent::WindowsDragDrop(window_id));
}
wry::DragDropEvent::Over { position } => {
_ = proxy.send_event(UserWindowEvent::WindowsDragOver(
window_id, position.0, position.1,
));
}
wry::DragDropEvent::Leave => {
_ = proxy.send_event(UserWindowEvent::WindowsDragLeave(window_id));
}
_ => {}
}
}
false
}
};
let page_loaded = AtomicBool::new(false);
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(
window.inner_size().width,
window.inner_size().height,
)),
})
.with_transparent(cfg.window.window.transparent)
.with_url("dioxus://index.html/")
.with_ipc_handler(ipc_handler)
.with_navigation_handler(move |var| {
if var.starts_with("dioxus://")
|| var.starts_with("http://dioxus.")
|| var.starts_with("https://dioxus.")
{
let page_loaded = page_loaded.swap(true, std::sync::atomic::Ordering::SeqCst);
!page_loaded
} else {
if var.starts_with("http://")
|| var.starts_with("https://")
|| var.starts_with("mailto:")
{
_ = webbrowser::open(&var);
}
false
}
}) .with_asynchronous_custom_protocol(String::from("dioxus"), request_handler);
#[cfg(target_os = "android")]
{
use wry::WebViewBuilderExtAndroid as _;
webview = webview.with_https_scheme(true);
};
#[cfg(target_os = "windows")]
{
use wry::WebViewBuilderExtWindows;
webview = webview.with_browser_accelerator_keys(false);
}
if !cfg.disable_file_drop_handler {
webview = webview.with_drag_drop_handler(file_drop_handler);
}
if let Some(color) = cfg.background_color {
webview = webview.with_background_color(color);
}
for (name, handler) in cfg.protocols.drain(..) {
#[cfg(feature = "tokio_runtime")]
let tokio_rt = tokio::runtime::Handle::current();
webview = webview.with_custom_protocol(name, move |a, b| {
#[cfg(feature = "tokio_runtime")]
let _guard = tokio_rt.enter();
handler(a, b)
});
}
for (name, handler) in cfg.asynchronous_protocols.drain(..) {
#[cfg(feature = "tokio_runtime")]
let tokio_rt = tokio::runtime::Handle::current();
webview = webview.with_asynchronous_custom_protocol(name, move |a, b, c| {
#[cfg(feature = "tokio_runtime")]
let _guard = tokio_rt.enter();
handler(a, b, c)
});
}
const INITIALIZATION_SCRIPT: &str = r#"
if (document.addEventListener) {
document.addEventListener('contextmenu', function(e) {
e.preventDefault();
}, false);
} else {
document.attachEvent('oncontextmenu', function() {
window.event.returnValue = false;
});
}
"#;
if cfg.disable_context_menu {
webview = webview.with_initialization_script(INITIALIZATION_SCRIPT)
} else {
webview = webview.with_devtools(true);
}
let menu = if cfg!(not(any(target_os = "android", target_os = "ios"))) {
let menu_option = cfg.menu.into();
if let Some(menu) = &menu_option {
crate::menubar::init_menu_bar(menu, &window);
}
menu_option
} else {
None
};
#[cfg(target_os = "windows")]
{
use wry::WebViewBuilderExtWindows;
if let Some(additional_windows_args) = &cfg.additional_windows_args {
webview = webview.with_additional_browser_args(additional_windows_args);
}
}
#[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,
shared.clone(),
asset_handlers,
file_hover,
cfg.window_close_behavior,
));
edits.set_desktop_context(Rc::downgrade(&desktop_context));
let provider: Rc<dyn Document> = Rc::new(DesktopDocument::new(desktop_context.clone()));
let history_provider: Rc<dyn History> = Rc::new(MemoryHistory::default());
dom.in_scope(ScopeId::ROOT, || {
provide_context(desktop_context.clone());
provide_context(provider);
provide_context(history_provider);
});
desktop_context.window.request_redraw();
WebviewInstance {
dom,
edits,
waker: tao_waker(shared.proxy.clone(), desktop_context.window.id()),
desktop_context,
_menu: menu,
_web_context: web_context,
}
}
pub fn poll_vdom(&mut self) {
let mut cx = std::task::Context::from_waker(&self.waker);
loop {
if self
.edits
.wry_queue
.poll_new_edits_location(&mut cx)
.is_ready()
{
_ = self.desktop_context.webview.evaluate_script(&format!(
"window.interpreter.waitForRequest(\"{edits_path}\", \"{expected_key}\");",
edits_path = self.edits.wry_queue.edits_path(),
expected_key = self.edits.wry_queue.required_server_key()
));
}
let edits_flushed_poll = self.edits.wry_queue.poll_edits_flushed(&mut cx);
if edits_flushed_poll.is_pending() {
return;
}
{
#[cfg(target_os = "android")]
let _lock = crate::android_sync_lock::android_runtime_lock();
let fut = self.dom.wait_for_work();
pin_mut!(fut);
match fut.poll_unpin(&mut cx) {
std::task::Poll::Ready(_) => {}
std::task::Poll::Pending => return,
}
}
#[cfg(target_os = "android")]
let _lock = crate::android_sync_lock::android_runtime_lock();
self.edits
.wry_queue
.with_mutation_state_mut(|f| self.dom.render_immediate(f));
self.edits.wry_queue.send_edits();
}
}
#[cfg(all(feature = "devtools", debug_assertions))]
pub fn kick_stylsheets(&self) {
_ = self
.desktop_context
.webview
.evaluate_script("window.interpreter.kickAllStylesheetsOnPage()");
}
pub(crate) fn show_toast(
&self,
header_text: &str,
message: &str,
level: &str,
duration: Duration,
after_reload: bool,
) {
let as_ms = duration.as_millis();
let js_fn_name = match after_reload {
true => "scheduleDXToast",
false => "showDXToast",
};
_ = self.desktop_context.webview.evaluate_script(&format!(
r#"
if (typeof {js_fn_name} !== "undefined") {{
window.{js_fn_name}("{header_text}", "{message}", "{level}", {as_ms});
}}
"#,
));
}
}
#[derive(serde::Serialize, Default)]
pub struct SynchronousEventResponse {
#[serde(rename = "preventDefault")]
prevent_default: bool,
}
impl SynchronousEventResponse {
#[allow(unused)]
pub fn new(prevent_default: bool) -> Self {
Self { prevent_default }
}
}
pub(crate) struct PendingWebview {
dom: VirtualDom,
cfg: Config,
sender: futures_channel::oneshot::Sender<DesktopContext>,
}
impl PendingWebview {
pub(crate) fn new(dom: VirtualDom, cfg: Config) -> (Self, PendingDesktopContext) {
let (sender, receiver) = futures_channel::oneshot::channel();
let webview = Self { dom, cfg, sender };
let pending = PendingDesktopContext { receiver };
(webview, pending)
}
pub(crate) fn create_window(self, shared: &Rc<SharedContext>) -> WebviewInstance {
let window = WebviewInstance::new(self.cfg, self.dom, shared.clone());
let cx = window
.dom
.in_scope(ScopeId::ROOT, consume_context::<Rc<DesktopService>>);
_ = self.sender.send(cx);
window
}
}