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

This commit is contained in:
Evan Almloff committed 2024-01-08 14:16:09 -06:00
commit 56798b3d1c
407 files changed
+16602 -11298

No files matched your search

+358
View File
@@ -0,0 +1,358 @@
use crate::{
config::{Config, WindowCloseBehaviour},
desktop_context::WindowEventHandlers,
element::DesktopElement,
file_upload::FileDialogRequest,
ipc::IpcMessage,
ipc::{EventData, UserWindowEvent},
query::QueryResult,
shortcut::{GlobalHotKeyEvent, ShortcutRegistry},
webview::WebviewInstance,
};
use crossbeam_channel::Receiver;
use dioxus_core::{Component, ElementId, VirtualDom};
use dioxus_html::{
native_bind::NativeFileEngine, FileEngine, HasFormData, HtmlEvent, MountedData,
PlatformEventData, SerializedHtmlEventConverter,
};
use std::{
cell::{Cell, RefCell},
collections::HashMap,
rc::Rc,
sync::Arc,
};
use tao::{
event::Event,
event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget},
window::WindowId,
};
/// The single top-level object that manages all the running windows, assets, shortcuts, etc
pub(crate) struct App<P> {
// move the props into a cell so we can pop it out later to create the first window
// iOS panics if we create a window before the event loop is started, so we toss them into a cell
pub(crate) props: Cell<Option<P>>,
pub(crate) cfg: Cell<Option<Config>>,
// Stuff we need mutable access to
pub(crate) root: Component<P>,
pub(crate) control_flow: ControlFlow,
pub(crate) is_visible_before_start: bool,
pub(crate) window_behavior: WindowCloseBehaviour,
pub(crate) webviews: HashMap<WindowId, WebviewInstance>,
/// This single blob of state is shared between all the windows so they have access to the runtime state
///
/// This includes stuff like the event handlers, shortcuts, etc as well as ways to modify *other* windows
pub(crate) shared: Rc<SharedContext>,
}
/// A bundle of state shared between all the windows, providing a way for us to communicate with running webview.
///
/// Todo: everything in this struct is wrapped in Rc<>, but we really only need the one top-level refcell
pub struct SharedContext {
pub(crate) event_handlers: WindowEventHandlers,
pub(crate) pending_webviews: RefCell<Vec<WebviewInstance>>,
pub(crate) shortcut_manager: ShortcutRegistry,
pub(crate) global_hotkey_channel: Receiver<GlobalHotKeyEvent>,
pub(crate) proxy: EventLoopProxy<UserWindowEvent>,
pub(crate) target: EventLoopWindowTarget<UserWindowEvent>,
}
impl<P: 'static> App<P> {
pub fn new(cfg: Config, props: P, root: Component<P>) -> (EventLoop<UserWindowEvent>, Self) {
let event_loop = EventLoopBuilder::<UserWindowEvent>::with_user_event().build();
let mut app = Self {
root,
window_behavior: cfg.last_window_close_behaviour,
is_visible_before_start: true,
webviews: HashMap::new(),
control_flow: ControlFlow::Wait,
props: Cell::new(Some(props)),
cfg: Cell::new(Some(cfg)),
shared: Rc::new(SharedContext {
event_handlers: WindowEventHandlers::default(),
pending_webviews: Default::default(),
shortcut_manager: ShortcutRegistry::new(),
global_hotkey_channel: GlobalHotKeyEvent::receiver().clone(),
proxy: event_loop.create_proxy(),
target: event_loop.clone(),
}),
};
// Copy over any assets we find
// todo - re-enable this when we have a faster way of copying assets
#[cfg(feature = "collect-assets")]
crate::collect_assets::copy_assets();
// Set the event converter
dioxus_html::set_event_converter(Box::new(SerializedHtmlEventConverter));
// Allow hotreloading to work - but only in debug mode
#[cfg(all(feature = "hot-reload", debug_assertions))]
app.connect_hotreload();
(event_loop, app)
}
pub fn tick(&mut self, window_event: &Event<'_, UserWindowEvent>) {
self.control_flow = ControlFlow::Wait;
self.shared
.event_handlers
.apply_event(window_event, &self.shared.target);
if let Ok(event) = self.shared.global_hotkey_channel.try_recv() {
self.shared.shortcut_manager.call_handlers(event);
}
}
#[cfg(all(feature = "hot-reload", debug_assertions))]
pub fn connect_hotreload(&mut self) {
dioxus_hot_reload::connect({
let proxy = self.shared.proxy.clone();
move |template| {
let _ = proxy.send_event(UserWindowEvent(
EventData::HotReloadEvent(template),
unsafe { WindowId::dummy() },
));
}
});
}
pub fn handle_new_window(&mut self) {
for handler in self.shared.pending_webviews.borrow_mut().drain(..) {
let id = handler.desktop_context.window.id();
self.webviews.insert(id, handler);
_ = self
.shared
.proxy
.send_event(UserWindowEvent(EventData::Poll, id));
}
}
pub fn handle_close_requested(&mut self, id: WindowId) {
use WindowCloseBehaviour::*;
match self.window_behavior {
LastWindowExitsApp => {
self.webviews.remove(&id);
if self.webviews.is_empty() {
self.control_flow = ControlFlow::Exit
}
}
LastWindowHides => {
let Some(webview) = self.webviews.get(&id) else {
return;
};
hide_app_window(&webview.desktop_context.webview);
}
CloseWindow => {
self.webviews.remove(&id);
}
}
}
pub fn window_destroyed(&mut self, id: WindowId) {
self.webviews.remove(&id);
if matches!(
self.window_behavior,
WindowCloseBehaviour::LastWindowExitsApp
) && self.webviews.is_empty()
{
self.control_flow = ControlFlow::Exit
}
}
pub fn handle_start_cause_init(&mut self) {
let props = self.props.take().unwrap();
let cfg = self.cfg.take().unwrap();
self.is_visible_before_start = cfg.window.window.visible;
let webview = WebviewInstance::new(
cfg,
VirtualDom::new_with_props(self.root, props),
self.shared.clone(),
);
let id = webview.desktop_context.window.id();
self.webviews.insert(id, webview);
_ = self
.shared
.proxy
.send_event(UserWindowEvent(EventData::Poll, id));
}
pub fn handle_browser_open(&mut self, msg: IpcMessage) {
if let Some(temp) = msg.params().as_object() {
if temp.contains_key("href") {
let open = webbrowser::open(temp["href"].as_str().unwrap());
if let Err(e) = open {
tracing::error!("Open Browser error: {:?}", e);
}
}
}
}
pub fn handle_initialize_msg(&mut self, id: WindowId) {
let view = self.webviews.get_mut(&id).unwrap();
view.desktop_context.send_edits(view.dom.rebuild());
view.desktop_context
.window
.set_visible(self.is_visible_before_start);
}
pub fn handle_close_msg(&mut self, id: WindowId) {
self.webviews.remove(&id);
if self.webviews.is_empty() {
self.control_flow = ControlFlow::Exit
}
}
pub fn handle_query_msg(&mut self, msg: IpcMessage, id: WindowId) {
let Ok(result) = serde_json::from_value::<QueryResult>(msg.params()) else {
return;
};
let Some(view) = self.webviews.get(&id) else {
return;
};
view.desktop_context.query.send(result);
}
pub fn handle_user_event_msg(&mut self, msg: IpcMessage, id: WindowId) {
let parsed_params = serde_json::from_value(msg.params())
.map_err(|err| tracing::error!("Error parsing user_event: {:?}", err));
let Ok(evt) = parsed_params else { return };
let HtmlEvent {
element,
name,
bubbles,
data,
} = evt;
let view = self.webviews.get_mut(&id).unwrap();
let query = view.desktop_context.query.clone();
// check for a mounted event placeholder and replace it with a desktop specific element
let as_any = match data {
dioxus_html::EventData::Mounted => {
let element = DesktopElement::new(element, view.desktop_context.clone(), query);
Rc::new(PlatformEventData::new(Box::new(MountedData::new(element))))
}
_ => data.into_any(),
};
view.dom.handle_event(&name, as_any, element, bubbles);
view.desktop_context.send_edits(view.dom.render_immediate());
}
#[cfg(all(feature = "hot-reload", debug_assertions))]
pub fn handle_hot_reload_msg(&mut self, msg: dioxus_hot_reload::HotReloadMsg) {
match msg {
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
for webview in self.webviews.values_mut() {
webview.dom.replace_template(template);
webview.poll_vdom();
}
}
dioxus_hot_reload::HotReloadMsg::Shutdown => {
self.control_flow = ControlFlow::Exit;
}
}
}
pub fn handle_file_dialog_msg(&mut self, msg: IpcMessage, window: WindowId) {
let Ok(file_dialog) = serde_json::from_value::<FileDialogRequest>(msg.params()) else {
return;
};
struct DesktopFileUploadForm {
files: Arc<NativeFileEngine>,
}
impl HasFormData for DesktopFileUploadForm {
fn files(&self) -> Option<Arc<dyn FileEngine>> {
Some(self.files.clone())
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
let id = ElementId(file_dialog.target);
let event_name = &file_dialog.event;
let event_bubbles = file_dialog.bubbles;
let files = file_dialog.get_file_event();
let data = Rc::new(PlatformEventData::new(Box::new(DesktopFileUploadForm {
files: Arc::new(NativeFileEngine::new(files)),
})));
let view = self.webviews.get_mut(&window).unwrap();
if event_name == "change&input" {
view.dom
.handle_event("input", data.clone(), id, event_bubbles);
view.dom.handle_event("change", data, id, event_bubbles);
} else {
view.dom.handle_event(event_name, data, id, event_bubbles);
}
view.desktop_context.send_edits(view.dom.render_immediate());
}
/// Poll the virtualdom until it's pending
///
/// The waker we give it is connected to the event loop, so it will wake up the event loop when it's ready to be polled again
///
/// All IO is done on the tokio runtime we started earlier
pub fn poll_vdom(&mut self, id: WindowId) {
let Some(view) = self.webviews.get_mut(&id) else {
return;
};
view.poll_vdom();
}
}
/// Different hide implementations per platform
#[allow(unused)]
pub fn hide_app_window(window: &wry::WebView) {
#[cfg(target_os = "windows")]
{
use tao::platform::windows::WindowExtWindows;
window.set_visible(false);
// window.set_skip_taskbar(true);
}
#[cfg(target_os = "linux")]
{
use tao::platform::unix::WindowExtUnix;
window.set_visible(false);
}
#[cfg(target_os = "macos")]
{
// window.set_visible(false); has the wrong behaviour on macOS
// It will hide the window but not show it again when the user switches
// back to the app. `NSApplication::hide:` has the correct behaviour
use objc::runtime::Object;
use objc::{msg_send, sel, sel_impl};
objc::rc::autoreleasepool(|| unsafe {
let app: *mut Object = msg_send![objc::class!(NSApplication), sharedApplication];
let nil = std::ptr::null_mut::<Object>();
let _: () = msg_send![app, hide: nil];
});
}
}
+61
View File
@@ -0,0 +1,61 @@
use dioxus_core::prelude::{Runtime, RuntimeGuard, ScopeId};
use rustc_hash::FxHashMap;
use std::{cell::RefCell, rc::Rc};
use wry::{http::Request, RequestAsyncResponder};
///
pub type AssetRequest = Request<Vec<u8>>;
pub struct AssetHandler {
f: Box<dyn Fn(AssetRequest, RequestAsyncResponder) + 'static>,
scope: ScopeId,
}
#[derive(Clone)]
pub struct AssetHandlerRegistry {
dom_rt: Rc<Runtime>,
handlers: Rc<RefCell<FxHashMap<String, AssetHandler>>>,
}
impl AssetHandlerRegistry {
pub fn new(dom_rt: Rc<Runtime>) -> Self {
AssetHandlerRegistry {
dom_rt,
handlers: Default::default(),
}
}
pub fn has_handler(&self, name: &str) -> bool {
self.handlers.borrow().contains_key(name)
}
pub fn handle_request(
&self,
name: &str,
request: AssetRequest,
responder: RequestAsyncResponder,
) {
if let Some(handler) = self.handlers.borrow().get(name) {
// make sure the runtime is alive for the duration of the handler
// We should do this for all the things - not just asset handlers
RuntimeGuard::with(self.dom_rt.clone(), Some(handler.scope), || {
(handler.f)(request, responder)
});
}
}
pub fn register_handler(
&self,
name: String,
f: Box<dyn Fn(AssetRequest, RequestAsyncResponder) + 'static>,
scope: ScopeId,
) {
self.handlers
.borrow_mut()
.insert(name, AssetHandler { f, scope });
}
pub fn remove_handler(&self, name: &str) -> Option<AssetHandler> {
self.handlers.borrow_mut().remove(name)
}
}
+60
View File
@@ -0,0 +1,60 @@
pub fn copy_assets() {
#[cfg(all(
debug_assertions,
any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)
))]
{
// The CLI will copy assets to the current working directory
if std::env::var_os("DIOXUS_ACTIVE").is_some() {
return;
}
use manganis_cli_support::AssetManifest;
use manganis_cli_support::AssetManifestExt;
use manganis_cli_support::Config;
use std::path::PathBuf;
let config = Config::current();
let asset_location = config.assets_serve_location();
let asset_location = PathBuf::from(asset_location);
let _ = std::fs::remove_dir_all(&asset_location);
println!("Finding assets... (Note: if you run a dioxus desktop application with the CLI. This process will be significantly faster.)");
let manifest = AssetManifest::load();
let has_assets = manifest
.packages()
.iter()
.any(|package| !package.assets().is_empty());
if has_assets {
println!("Copying and optimizing assets...");
manifest.copy_static_assets_to(&asset_location).unwrap();
println!("Copied assets to {}", asset_location.display());
} else {
println!("No assets found");
}
}
#[cfg(not(all(
debug_assertions,
any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)
)))]
{
println!(
"Skipping assets in release mode. You compile assets with the dioxus-cli in release mode"
);
}
}
@@ -1,16 +1,13 @@
use std::borrow::Cow;
use std::path::PathBuf;
use wry::application::window::Icon;
use dioxus_core::prelude::Component;
use tao::window::{Icon, WindowBuilder, WindowId};
use wry::{
application::window::{Window, WindowBuilder},
http::{Request as HttpRequest, Response as HttpResponse},
webview::FileDropEvent,
Result as WryResult,
FileDropEvent,
};
// pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView);
/// The behaviour of the application when the last window is closed.
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum WindowCloseBehaviour {
@@ -36,13 +33,14 @@ pub struct Config {
pub(crate) root_name: String,
pub(crate) background_color: Option<(u8, u8, u8, u8)>,
pub(crate) last_window_close_behaviour: WindowCloseBehaviour,
pub(crate) enable_default_menu_bar: bool,
}
type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>;
type DropHandler = Box<dyn Fn(WindowId, FileDropEvent) -> bool>;
pub(crate) type WryProtocol = (
String,
Box<dyn Fn(&HttpRequest<Vec<u8>>) -> WryResult<HttpResponse<Cow<'static, [u8]>>> + 'static>,
Box<dyn Fn(HttpRequest<Vec<u8>>) -> HttpResponse<Cow<'static, [u8]>> + 'static>,
);
impl Config {
@@ -65,9 +63,32 @@ impl Config {
root_name: "main".to_string(),
background_color: None,
last_window_close_behaviour: WindowCloseBehaviour::LastWindowExitsApp,
enable_default_menu_bar: true,
}
}
/// Launch a Dioxus app using the given component and config
///
/// See the [`crate::launch::launch`] function for more details.
pub fn launch(self, root: Component<()>) {
crate::launch::launch_cfg(root, self)
}
/// Launch a Dioxus app using the given component, config, and props
///
/// See the [`crate::launch::launch_with_props`] function for more details.
pub fn launch_with_props<P: 'static>(self, root: Component<P>, props: P) {
crate::launch::launch_with_props(root, props, self)
}
/// Set whether the default menu bar should be enabled.
///
/// > Note: `enable` is `true` by default. To disable the default menu bar pass `false`.
pub fn with_default_menu_bar(mut self, enable: bool) -> Self {
self.enable_default_menu_bar = enable;
self
}
/// set the directory from which assets will be searched in release mode
pub fn with_resource_directory(mut self, path: impl Into<PathBuf>) -> Self {
self.resource_dir = Some(path.into());
@@ -111,7 +132,7 @@ impl Config {
/// Set a file drop handler. If this is enabled, html drag events will be disabled.
pub fn with_file_drop_handler(
mut self,
handler: impl Fn(&Window, FileDropEvent) -> bool + 'static,
handler: impl Fn(WindowId, FileDropEvent) -> bool + 'static,
) -> Self {
self.file_drop_handler = Some(Box::new(handler));
self
@@ -120,7 +141,7 @@ impl Config {
/// Set a custom protocol
pub fn with_custom_protocol<F>(mut self, name: String, handler: F) -> Self
where
F: Fn(&HttpRequest<Vec<u8>>) -> WryResult<HttpResponse<Cow<'static, [u8]>>> + 'static,
F: Fn(HttpRequest<Vec<u8>>) -> HttpResponse<Cow<'static, [u8]>> + 'static,
{
self.protocols.push((name, Box::new(handler)));
self
+132 -151
View File
@@ -1,41 +1,42 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::rc::Weak;
use crate::create_new_window;
use crate::events::IpcMessage;
use crate::query::QueryEngine;
use crate::shortcut::ShortcutId;
use crate::shortcut::ShortcutRegistry;
use crate::shortcut::ShortcutRegistryError;
use crate::Config;
use crate::WebviewHandler;
use dioxus_core::ScopeState;
use dioxus_core::VirtualDom;
#[cfg(all(feature = "hot-reload", debug_assertions))]
use dioxus_hot_reload::HotReloadMsg;
use crate::{
app::SharedContext,
assets::AssetHandlerRegistry,
edits::EditQueue,
ipc::{EventData, UserWindowEvent},
query::QueryEngine,
shortcut::{HotKey, ShortcutId, ShortcutRegistryError},
webview::WebviewInstance,
AssetRequest, Config,
};
use dioxus_core::{
prelude::{current_scope_id, ScopeId},
Mutations, VirtualDom,
};
use dioxus_interpreter_js::binary_protocol::Channel;
use rustc_hash::FxHashMap;
use slab::Slab;
use wry::application::accelerator::Accelerator;
use wry::application::event::Event;
use wry::application::event_loop::EventLoopProxy;
use wry::application::event_loop::EventLoopWindowTarget;
use std::{cell::RefCell, fmt::Debug, rc::Rc, rc::Weak, sync::atomic::AtomicU16};
use tao::{
event::Event,
event_loop::EventLoopWindowTarget,
window::{Fullscreen as WryFullscreen, Window, WindowId},
};
use wry::{RequestAsyncResponder, WebView};
#[cfg(target_os = "ios")]
use wry::application::platform::ios::WindowExtIOS;
use wry::application::window::Fullscreen as WryFullscreen;
use wry::application::window::Window;
use wry::application::window::WindowId;
use wry::webview::WebView;
use tao::platform::ios::WindowExtIOS;
pub type ProxyType = EventLoopProxy<UserWindowEvent>;
/// Get an imperative handle to the current window
pub fn use_window(cx: &ScopeState) -> &DesktopContext {
cx.use_hook(|| cx.consume_context::<DesktopContext>())
.as_ref()
.unwrap()
/// Get an imperative handle to the current window without using a hook
///
/// ## Panics
///
/// This function will panic if it is called outside of the context of a Dioxus App.
pub fn window() -> DesktopContext {
dioxus_core::prelude::consume_context().unwrap()
}
pub(crate) type WebviewQueue = Rc<RefCell<Vec<WebviewHandler>>>;
/// A handle to the [`DesktopService`] that can be passed around.
pub type DesktopContext = Rc<DesktopService>;
/// An imperative interface to the current window.
///
@@ -51,60 +52,69 @@ pub(crate) type WebviewQueue = Rc<RefCell<Vec<WebviewHandler>>>;
/// ```
pub struct DesktopService {
/// The wry/tao proxy to the current window
pub webview: Rc<WebView>,
pub webview: WebView,
/// The proxy to the event loop
pub proxy: ProxyType,
/// The tao window itself
pub window: Window,
pub(crate) shared: Rc<SharedContext>,
/// The receiver for queries about the current window
pub(super) query: QueryEngine,
pub(super) pending_windows: WebviewQueue,
pub(crate) event_loop: EventLoopWindowTarget<UserWindowEvent>,
pub(crate) event_handlers: WindowEventHandlers,
pub(crate) shortcut_manager: ShortcutRegistry,
pub(crate) edit_queue: EditQueue,
pub(crate) templates: RefCell<FxHashMap<String, u16>>,
pub(crate) max_template_count: AtomicU16,
pub(crate) channel: RefCell<Channel>,
pub(crate) asset_handlers: AssetHandlerRegistry,
#[cfg(target_os = "ios")]
pub(crate) views: Rc<RefCell<Vec<*mut objc::runtime::Object>>>,
}
/// A handle to the [`DesktopService`] that can be passed around.
pub type DesktopContext = Rc<DesktopService>;
/// A smart pointer to the current window.
impl std::ops::Deref for DesktopService {
type Target = Window;
fn deref(&self) -> &Self::Target {
self.webview.window()
&self.window
}
}
impl DesktopService {
pub(crate) fn new(
webview: WebView,
proxy: ProxyType,
event_loop: EventLoopWindowTarget<UserWindowEvent>,
webviews: WebviewQueue,
event_handlers: WindowEventHandlers,
shortcut_manager: ShortcutRegistry,
window: Window,
shared: Rc<SharedContext>,
edit_queue: EditQueue,
asset_handlers: AssetHandlerRegistry,
) -> Self {
Self {
webview: Rc::new(webview),
proxy,
event_loop,
window,
webview,
shared,
edit_queue,
asset_handlers,
query: Default::default(),
pending_windows: webviews,
event_handlers,
shortcut_manager,
templates: Default::default(),
max_template_count: Default::default(),
channel: Default::default(),
#[cfg(target_os = "ios")]
views: Default::default(),
}
}
/// Send a list of mutations to the webview
pub(crate) fn send_edits(&self, edits: Mutations) {
if let Some(bytes) = crate::edits::apply_edits(
edits,
&mut self.channel.borrow_mut(),
&mut self.templates.borrow_mut(),
&self.max_template_count,
) {
self.edit_queue.add_edits(bytes)
}
}
/// Create a new window using the props and window builder
///
/// Returns the webview handle for the new window.
@@ -113,35 +123,23 @@ impl DesktopService {
///
/// Be careful to not create a cycle of windows, or you might leak memory.
pub fn new_window(&self, dom: VirtualDom, cfg: Config) -> Weak<DesktopService> {
let window = create_new_window(
cfg,
&self.event_loop,
&self.proxy,
dom,
&self.pending_windows,
&self.event_handlers,
self.shortcut_manager.clone(),
);
let window = WebviewInstance::new(cfg, dom, self.shared.clone());
let desktop_context = window
.dom
.base_scope()
.consume_context::<Rc<DesktopService>>()
let cx = window.desktop_context.clone();
self.shared
.proxy
.send_event(UserWindowEvent(EventData::NewWindow, cx.id()))
.unwrap();
let id = window.desktop_context.webview.window().id();
self.proxy
.send_event(UserWindowEvent(EventData::NewWindow, id))
self.shared
.proxy
.send_event(UserWindowEvent(EventData::Poll, cx.id()))
.unwrap();
self.proxy
.send_event(UserWindowEvent(EventData::Poll, id))
.unwrap();
self.shared.pending_webviews.borrow_mut().push(window);
self.pending_windows.borrow_mut().push(window);
Rc::downgrade(&desktop_context)
Rc::downgrade(&cx)
}
/// trigger the drag-window event
@@ -153,41 +151,38 @@ impl DesktopService {
/// onmousedown: move |_| { desktop.drag_window(); }
/// ```
pub fn drag(&self) {
let window = self.webview.window();
// if the drag_window has any errors, we don't do anything
if window.fullscreen().is_none() {
window.drag_window().unwrap();
if self.window.fullscreen().is_none() {
_ = self.window.drag_window();
}
}
/// Toggle whether the window is maximized or not
pub fn toggle_maximized(&self) {
let window = self.webview.window();
window.set_maximized(!window.is_maximized())
self.window.set_maximized(!self.window.is_maximized())
}
/// close window
/// Close this window
pub fn close(&self) {
let _ = self
.shared
.proxy
.send_event(UserWindowEvent(EventData::CloseWindow, self.id()));
}
/// close window
/// Close a particular window, given its ID
pub fn close_window(&self, id: WindowId) {
let _ = self
.shared
.proxy
.send_event(UserWindowEvent(EventData::CloseWindow, id));
}
/// change window to fullscreen
pub fn set_fullscreen(&self, fullscreen: bool) {
if let Some(handle) = self.webview.window().current_monitor() {
self.webview
.window()
.set_fullscreen(fullscreen.then_some(WryFullscreen::Borderless(Some(handle))));
if let Some(handle) = &self.window.current_monitor() {
self.window.set_fullscreen(
fullscreen.then_some(WryFullscreen::Borderless(Some(handle.clone()))),
);
}
}
@@ -220,12 +215,12 @@ impl DesktopService {
&self,
handler: impl FnMut(&Event<UserWindowEvent>, &EventLoopWindowTarget<UserWindowEvent>) + 'static,
) -> WryEventHandlerId {
self.event_handlers.add(self.id(), handler)
self.shared.event_handlers.add(self.window.id(), handler)
}
/// Remove a wry event handler created with [`DesktopContext::create_wry_event_handler`]
pub fn remove_wry_event_handler(&self, id: WryEventHandlerId) {
self.event_handlers.remove(id)
self.shared.event_handlers.remove(id)
}
/// Create a global shortcut
@@ -233,27 +228,55 @@ impl DesktopService {
/// Linux: Only works on x11. See [this issue](https://github.com/tauri-apps/tao/issues/331) for more information.
pub fn create_shortcut(
&self,
accelerator: Accelerator,
hotkey: HotKey,
callback: impl FnMut() + 'static,
) -> Result<ShortcutId, ShortcutRegistryError> {
self.shortcut_manager
.add_shortcut(accelerator, Box::new(callback))
self.shared
.shortcut_manager
.add_shortcut(hotkey, Box::new(callback))
}
/// Remove a global shortcut
pub fn remove_shortcut(&self, id: ShortcutId) {
self.shortcut_manager.remove_shortcut(id)
self.shared.shortcut_manager.remove_shortcut(id)
}
/// Remove all global shortcuts
pub fn remove_all_shortcuts(&self) {
self.shortcut_manager.remove_all()
self.shared.shortcut_manager.remove_all()
}
/// Provide a callback to handle asset loading yourself.
/// If the ScopeId isn't provided, defaults to a global handler.
/// Note that the handler is namespaced by name, not ScopeId.
///
/// When the component is dropped, the handler is removed.
///
/// See [`use_asset_handle`](crate::use_asset_handle) for a convenient hook.
pub fn register_asset_handler(
&self,
name: String,
f: Box<dyn Fn(AssetRequest, RequestAsyncResponder) + 'static>,
scope: Option<ScopeId>,
) {
self.asset_handlers.register_handler(
name,
f,
scope.unwrap_or(current_scope_id().unwrap_or(ScopeId(0))),
)
}
/// Removes an asset handler by its identifier.
///
/// Returns `None` if the handler did not exist.
pub fn remove_asset_handler(&self, name: &str) -> Option<()> {
self.asset_handlers.remove_handler(name).map(|_| ())
}
/// Push an objc view to the window
#[cfg(target_os = "ios")]
pub fn push_view(&self, view: objc_id::ShareId<objc::runtime::Object>) {
let window = self.webview.window();
let window = &self.window;
unsafe {
use objc::runtime::Object;
@@ -273,7 +296,7 @@ impl DesktopService {
/// Pop an objc view from the window
#[cfg(target_os = "ios")]
pub fn pop_view(&self) {
let window = self.webview.window();
let window = &self.window;
unsafe {
use objc::runtime::Object;
@@ -287,23 +310,6 @@ impl DesktopService {
}
}
#[derive(Debug, Clone)]
pub struct UserWindowEvent(pub EventData, pub WindowId);
#[derive(Debug, Clone)]
pub enum EventData {
Poll,
Ipc(IpcMessage),
#[cfg(all(feature = "hot-reload", debug_assertions))]
HotReloadEvent(HotReloadMsg),
NewWindow,
CloseWindow,
}
#[cfg(target_os = "ios")]
fn is_main_thread() -> bool {
use objc::runtime::{Class, BOOL, NO};
@@ -369,45 +375,20 @@ impl WryWindowEventHandlerInner {
target: &EventLoopWindowTarget<UserWindowEvent>,
) {
// if this event does not apply to the window this listener cares about, return
match event {
Event::WindowEvent { window_id, .. }
| Event::MenuEvent {
window_id: Some(window_id),
..
} => {
if *window_id != self.window_id {
return;
}
if let Event::WindowEvent { window_id, .. } = event {
if *window_id != self.window_id {
return;
}
_ => (),
}
(self.handler)(event, target)
}
}
/// Get a closure that executes any JavaScript in the WebView context.
pub fn use_wry_event_handler(
cx: &ScopeState,
handler: impl FnMut(&Event<UserWindowEvent>, &EventLoopWindowTarget<UserWindowEvent>) + 'static,
) -> &WryEventHandler {
let desktop = use_window(cx);
cx.use_hook(move || {
let desktop = desktop.clone();
let id = desktop.create_wry_event_handler(handler);
WryEventHandler {
handlers: desktop.event_handlers.clone(),
id,
}
})
}
/// A wry event handler that is scoped to the current component and window. The event handler will only receive events for the window it was created for and global events.
///
/// This will automatically be removed when the component is unmounted.
pub struct WryEventHandler {
handlers: WindowEventHandlers,
pub(crate) handlers: WindowEventHandlers,
/// The unique identifier of the event handler.
pub id: WryEventHandlerId,
}
+172
View File
@@ -0,0 +1,172 @@
use dioxus_core::{BorrowedAttributeValue, Mutations, Template, TemplateAttribute, TemplateNode};
use dioxus_html::event_bubbles;
use dioxus_interpreter_js::binary_protocol::Channel;
use rustc_hash::FxHashMap;
use std::{
sync::atomic::AtomicU16,
sync::Arc,
sync::{atomic::Ordering, Mutex},
};
use wry::RequestAsyncResponder;
/// 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)]
pub(crate) struct EditQueue {
queue: Arc<Mutex<Vec<Vec<u8>>>>,
responder: Arc<Mutex<Option<RequestAsyncResponder>>>,
}
impl EditQueue {
pub fn handle_request(&self, responder: RequestAsyncResponder) {
let mut queue = self.queue.lock().unwrap();
if let Some(bytes) = queue.pop() {
responder.respond(wry::http::Response::new(bytes));
} else {
*self.responder.lock().unwrap() = Some(responder);
}
}
pub fn add_edits(&self, edits: Vec<u8>) {
let mut responder = self.responder.lock().unwrap();
if let Some(responder) = responder.take() {
responder.respond(wry::http::Response::new(edits));
} else {
self.queue.lock().unwrap().push(edits);
}
}
}
pub(crate) fn apply_edits(
mutations: Mutations,
channel: &mut Channel,
templates: &mut FxHashMap<String, u16>,
max_template_count: &AtomicU16,
) -> Option<Vec<u8>> {
if mutations.templates.is_empty() && mutations.edits.is_empty() {
return None;
}
for template in mutations.templates {
add_template(&template, channel, templates, max_template_count);
}
use dioxus_core::Mutation::*;
for edit in mutations.edits {
match edit {
AppendChildren { id, m } => channel.append_children(id.0 as u32, m as u16),
AssignId { path, id } => channel.assign_id(path, id.0 as u32),
CreatePlaceholder { id } => channel.create_placeholder(id.0 as u32),
CreateTextNode { value, id } => channel.create_text_node(value, id.0 as u32),
HydrateText { path, value, id } => channel.hydrate_text(path, value, id.0 as u32),
LoadTemplate { name, index, id } => {
if let Some(tmpl_id) = templates.get(name) {
channel.load_template(*tmpl_id, index as u16, id.0 as u32)
}
}
ReplaceWith { id, m } => channel.replace_with(id.0 as u32, m as u16),
ReplacePlaceholder { path, m } => channel.replace_placeholder(path, m as u16),
InsertAfter { id, m } => channel.insert_after(id.0 as u32, m as u16),
InsertBefore { id, m } => channel.insert_before(id.0 as u32, m as u16),
SetAttribute {
name,
value,
id,
ns,
} => match value {
BorrowedAttributeValue::Text(txt) => {
channel.set_attribute(id.0 as u32, name, txt, ns.unwrap_or_default())
}
BorrowedAttributeValue::Float(f) => {
channel.set_attribute(id.0 as u32, name, &f.to_string(), ns.unwrap_or_default())
}
BorrowedAttributeValue::Int(n) => {
channel.set_attribute(id.0 as u32, name, &n.to_string(), ns.unwrap_or_default())
}
BorrowedAttributeValue::Bool(b) => channel.set_attribute(
id.0 as u32,
name,
if b { "true" } else { "false" },
ns.unwrap_or_default(),
),
BorrowedAttributeValue::None => {
channel.remove_attribute(id.0 as u32, name, ns.unwrap_or_default())
}
_ => unreachable!(),
},
SetText { value, id } => channel.set_text(id.0 as u32, value),
NewEventListener { name, id, .. } => {
channel.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
}
RemoveEventListener { name, id } => {
channel.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
}
Remove { id } => channel.remove(id.0 as u32),
PushRoot { id } => channel.push_root(id.0 as u32),
}
}
let bytes: Vec<_> = channel.export_memory().collect();
channel.reset();
Some(bytes)
}
pub fn add_template(
template: &Template<'static>,
channel: &mut Channel,
templates: &mut FxHashMap<String, u16>,
max_template_count: &AtomicU16,
) {
let current_max_template_count = max_template_count.load(Ordering::Relaxed);
for root in template.roots.iter() {
create_template_node(channel, root);
templates.insert(template.name.to_owned(), current_max_template_count);
}
channel.add_templates(current_max_template_count, template.roots.len() as u16);
max_template_count.fetch_add(1, Ordering::Relaxed);
}
pub fn create_template_node(channel: &mut Channel, node: &'static TemplateNode<'static>) {
use TemplateNode::*;
match node {
Element {
tag,
namespace,
attrs,
children,
..
} => {
// Push the current node onto the stack
match namespace {
Some(ns) => channel.create_element_ns(tag, ns),
None => channel.create_element(tag),
}
// Set attributes on the current node
for attr in *attrs {
if let TemplateAttribute::Static {
name,
value,
namespace,
} = attr
{
channel.set_top_attribute(name, value, namespace.unwrap_or_default())
}
}
// Add each child to the stack
for child in *children {
create_template_node(channel, child);
}
// Add all children to the parent
channel.append_children_to_top(children.len() as u16);
}
Text { text } => channel.create_raw_text(text),
DynamicText { .. } => channel.create_raw_text("p"),
Dynamic { .. } => channel.add_placeholder(),
}
}
+6 -5
View File
@@ -3,6 +3,7 @@ use dioxus_html::{geometry::euclid::Rect, MountedResult, RenderedElementBacking}
use crate::{desktop_context::DesktopContext, query::QueryEngine};
#[derive(Clone)]
/// A mounted element passed to onmounted events
pub struct DesktopElement {
id: ElementId,
@@ -17,8 +18,8 @@ impl DesktopElement {
}
impl RenderedElementBacking for DesktopElement {
fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> {
Ok(self)
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn get_client_rect(
@@ -30,7 +31,7 @@ impl RenderedElementBacking for DesktopElement {
>,
>,
> {
let script = format!("return window.interpreter.GetClientRect({});", self.id.0);
let script = format!("return window.interpreter.getClientRect({});", self.id.0);
let fut = self
.query
@@ -54,7 +55,7 @@ impl RenderedElementBacking for DesktopElement {
behavior: dioxus_html::ScrollBehavior,
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
let script = format!(
"return window.interpreter.ScrollTo({}, {});",
"return window.interpreter.scrollTo({}, {});",
self.id.0,
serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior")
);
@@ -81,7 +82,7 @@ impl RenderedElementBacking for DesktopElement {
focus: bool,
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
let script = format!(
"return window.interpreter.SetFocus({}, {});",
"return window.interpreter.setFocus({}, {});",
self.id.0, focus
);
+7 -9
View File
@@ -1,21 +1,19 @@
#![allow(clippy::await_holding_refcell_ref)]
use async_trait::async_trait;
use dioxus_core::ScopeState;
use dioxus_html::prelude::{EvalError, EvalProvider, Evaluator};
use std::{cell::RefCell, rc::Rc};
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);
}
/// Reprents the desktop-target's provider of evaluators.
pub struct DesktopEvalProvider {
desktop_ctx: DesktopContext,
pub(crate) desktop_ctx: DesktopContext,
}
impl DesktopEvalProvider {
pub fn new(desktop_ctx: DesktopContext) -> Self {
Self { desktop_ctx }
}
}
impl EvalProvider for DesktopEvalProvider {
+140 -11
View File
@@ -1,19 +1,148 @@
//! Convert a serialized event to an event trigger
use serde::{Deserialize, Serialize};
use crate::element::DesktopElement;
use dioxus_html::*;
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct IpcMessage {
method: String,
params: serde_json::Value,
}
pub(crate) struct SerializedHtmlEventConverter;
impl IpcMessage {
pub(crate) fn method(&self) -> &str {
self.method.as_str()
impl HtmlEventConverter for SerializedHtmlEventConverter {
fn convert_animation_data(&self, event: &PlatformEventData) -> AnimationData {
event
.downcast::<SerializedAnimationData>()
.cloned()
.unwrap()
.into()
}
pub(crate) fn params(self) -> serde_json::Value {
self.params
fn convert_clipboard_data(&self, event: &PlatformEventData) -> ClipboardData {
event
.downcast::<SerializedClipboardData>()
.cloned()
.unwrap()
.into()
}
fn convert_composition_data(&self, event: &PlatformEventData) -> CompositionData {
event
.downcast::<SerializedCompositionData>()
.cloned()
.unwrap()
.into()
}
fn convert_drag_data(&self, event: &PlatformEventData) -> DragData {
event
.downcast::<SerializedDragData>()
.cloned()
.unwrap()
.into()
}
fn convert_focus_data(&self, event: &PlatformEventData) -> FocusData {
event
.downcast::<SerializedFocusData>()
.cloned()
.unwrap()
.into()
}
fn convert_form_data(&self, event: &PlatformEventData) -> FormData {
event
.downcast::<SerializedFormData>()
.cloned()
.unwrap()
.into()
}
fn convert_image_data(&self, event: &PlatformEventData) -> ImageData {
event
.downcast::<SerializedImageData>()
.cloned()
.unwrap()
.into()
}
fn convert_keyboard_data(&self, event: &PlatformEventData) -> KeyboardData {
event
.downcast::<SerializedKeyboardData>()
.cloned()
.unwrap()
.into()
}
fn convert_media_data(&self, event: &PlatformEventData) -> MediaData {
event
.downcast::<SerializedMediaData>()
.cloned()
.unwrap()
.into()
}
fn convert_mounted_data(&self, event: &PlatformEventData) -> MountedData {
event.downcast::<DesktopElement>().cloned().unwrap().into()
}
fn convert_mouse_data(&self, event: &PlatformEventData) -> MouseData {
event
.downcast::<SerializedMouseData>()
.cloned()
.unwrap()
.into()
}
fn convert_pointer_data(&self, event: &PlatformEventData) -> PointerData {
event
.downcast::<SerializedPointerData>()
.cloned()
.unwrap()
.into()
}
fn convert_scroll_data(&self, event: &PlatformEventData) -> ScrollData {
event
.downcast::<SerializedScrollData>()
.cloned()
.unwrap()
.into()
}
fn convert_selection_data(&self, event: &PlatformEventData) -> SelectionData {
event
.downcast::<SerializedSelectionData>()
.cloned()
.unwrap()
.into()
}
fn convert_toggle_data(&self, event: &PlatformEventData) -> ToggleData {
event
.downcast::<SerializedToggleData>()
.cloned()
.unwrap()
.into()
}
fn convert_touch_data(&self, event: &PlatformEventData) -> TouchData {
event
.downcast::<SerializedTouchData>()
.cloned()
.unwrap()
.into()
}
fn convert_transition_data(&self, event: &PlatformEventData) -> TransitionData {
event
.downcast::<SerializedTransitionData>()
.cloned()
.unwrap()
.into()
}
fn convert_wheel_data(&self, event: &PlatformEventData) -> WheelData {
event
.downcast::<SerializedWheelData>()
.cloned()
.unwrap()
.into()
}
}
+64 -61
View File
@@ -14,74 +14,77 @@ pub(crate) struct FileDialogRequest {
pub bubbles: bool,
}
#[cfg(not(any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)))]
pub(crate) fn get_file_event(_request: &FileDialogRequest) -> Vec<PathBuf> {
vec![]
}
#[allow(unused)]
impl FileDialogRequest {
#[cfg(not(any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)))]
pub(crate) fn get_file_event(&self) -> Vec<PathBuf> {
vec![]
}
#[cfg(any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
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()
#[cfg(any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
pub(crate) fn get_file_event(&self) -> 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();
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();
let file_extensions: Vec<_> = filters
.iter()
.flat_map(|f| f.as_extensions().into_iter())
.collect();
dialog = dialog.add_filter("name", file_extensions.as_slice());
dialog = dialog.add_filter("name", file_extensions.as_slice());
let files: Vec<_> = if request.multiple {
dialog.pick_files().into_iter().flatten().collect()
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 self.directory {
get_file_event_for_folder(self, dialog)
} else {
dialog.pick_file().into_iter().collect()
};
files
}
let dialog = rfd::FileDialog::new();
if request.directory {
get_file_event_for_folder(request, dialog)
} else {
get_file_event_for_file(request, dialog)
get_file_event_for_file(self, dialog)
}
}
}
+77
View File
@@ -0,0 +1,77 @@
use crate::{
assets::*, ipc::UserWindowEvent, shortcut::IntoAccelerator, window, DesktopContext,
ShortcutHandle, ShortcutRegistryError, WryEventHandler,
};
use dioxus_core::ScopeState;
use tao::{event::Event, event_loop::EventLoopWindowTarget};
use wry::RequestAsyncResponder;
/// Get an imperative handle to the current window
pub fn use_window(cx: &ScopeState) -> &DesktopContext {
cx.use_hook(|| cx.consume_context::<DesktopContext>())
.as_ref()
.unwrap()
}
/// Get a closure that executes any JavaScript in the WebView context.
pub fn use_wry_event_handler(
cx: &ScopeState,
handler: impl FnMut(&Event<UserWindowEvent>, &EventLoopWindowTarget<UserWindowEvent>) + 'static,
) -> &WryEventHandler {
cx.use_hook(move || {
let desktop = window();
let id = desktop.create_wry_event_handler(handler);
WryEventHandler {
handlers: desktop.shared.event_handlers.clone(),
id,
}
})
}
/// Provide a callback to handle asset loading yourself.
///
/// The callback takes a path as requested by the web view, and it should return `Some(response)`
/// if you want to load the asset, and `None` if you want to fallback on the default behavior.
pub fn use_asset_handler(
cx: &ScopeState,
name: &str,
handler: impl Fn(AssetRequest, RequestAsyncResponder) + 'static,
) {
cx.use_hook(|| {
crate::window().asset_handlers.register_handler(
name.to_string(),
Box::new(handler),
cx.scope_id(),
);
Handler(name.to_string())
});
// todo: can we just put ondrop in core?
struct Handler(String);
impl Drop for Handler {
fn drop(&mut self) {
_ = crate::window().asset_handlers.remove_handler(&self.0);
}
}
}
/// Get a closure that executes any JavaScript in the WebView context.
pub fn use_global_shortcut(
cx: &ScopeState,
accelerator: impl IntoAccelerator,
handler: impl FnMut() + 'static,
) -> &Result<ShortcutHandle, ShortcutRegistryError> {
cx.use_hook(move || {
let desktop = window();
let id = desktop.create_shortcut(accelerator.accelerator(), handler);
Ok(ShortcutHandle {
desktop,
shortcut_id: id?,
})
})
}
+9 -9
View File
@@ -1,12 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>Dioxus app</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- CUSTOM HEAD -->
</head>
<body>
<div id="main"></div>
<!-- MODULE LOADER -->
</body>
<head>
<title>Dioxus app</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- CUSTOM HEAD -->
</head>
<body>
<div id="main"></div>
<!-- MODULE LOADER -->
</body>
</html>
+64
View File
@@ -0,0 +1,64 @@
use serde::{Deserialize, Serialize};
use tao::window::WindowId;
/// A pair of data
#[derive(Debug, Clone)]
pub struct UserWindowEvent(pub EventData, pub WindowId);
/// The data that might eminate from any window/webview
#[derive(Debug, Clone)]
pub enum EventData {
/// Poll the virtualdom
Poll,
/// Handle an ipc message eminating from the window.postMessage of a given webview
Ipc(IpcMessage),
/// Handle a hotreload event, basically telling us to update our templates
#[cfg(all(feature = "hot-reload", debug_assertions))]
HotReloadEvent(dioxus_hot_reload::HotReloadMsg),
/// Create a new window
NewWindow,
/// Close a given window (could be any window!)
CloseWindow,
}
/// A message struct that manages the communication between the webview and the eventloop code
///
/// This needs to be serializable across the JS boundary, so the method names and structs are sensitive.
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct IpcMessage {
method: String,
params: serde_json::Value,
}
/// A set of known messages that we need to respond to
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum IpcMethod<'a> {
FileDialog,
UserEvent,
Query,
BrowserOpen,
Initialize,
Other(&'a str),
}
impl IpcMessage {
pub(crate) fn method(&self) -> IpcMethod {
match self.method.as_str() {
// todo: this is a misspelling, needs to be fixed
"file_diolog" => IpcMethod::FileDialog,
"user_event" => IpcMethod::UserEvent,
"query" => IpcMethod::Query,
"browser_open" => IpcMethod::BrowserOpen,
"initialize" => IpcMethod::Initialize,
_ => IpcMethod::Other(&self.method),
}
}
pub(crate) fn params(self) -> serde_json::Value {
self.params
}
}
+131
View File
@@ -0,0 +1,131 @@
use crate::{
app::App,
ipc::{EventData, IpcMethod, UserWindowEvent},
Config,
};
use dioxus_core::*;
use tao::event::{Event, StartCause, WindowEvent};
/// Launch the WebView and run the event loop.
///
/// This function will start a multithreaded Tokio runtime as well the WebView event loop.
///
/// ```rust, no_run
/// use dioxus::prelude::*;
///
/// fn main() {
/// dioxus_desktop::launch(app);
/// }
///
/// fn app(cx: Scope) -> Element {
/// cx.render(rsx!{
/// h1 {"hello world!"}
/// })
/// }
/// ```
pub fn launch(root: Component) {
launch_with_props(root, (), Config::default())
}
/// Launch the WebView and run the event loop, with configuration.
///
/// This function will start a multithreaded Tokio runtime as well the WebView event loop.
///
/// You can configure the WebView window with a configuration closure
///
/// ```rust, no_run
/// use dioxus::prelude::*;
/// use dioxus_desktop::*;
///
/// fn main() {
/// dioxus_desktop::launch_cfg(app, Config::default().with_window(WindowBuilder::new().with_title("My App")));
/// }
///
/// fn app(cx: Scope) -> Element {
/// cx.render(rsx!{
/// h1 {"hello world!"}
/// })
/// }
/// ```
pub fn launch_cfg(root: Component, config_builder: Config) {
launch_with_props(root, (), config_builder)
}
/// Launch the WebView and run the event loop, with configuration and root props.
///
/// If the [`tokio`] feature is enabled, this will also startup and block a tokio runtime using the unconstrained task.
/// This function will start a multithreaded Tokio runtime as well the WebView event loop. This will block the current thread.
///
/// You can configure the WebView window with a configuration closure
///
/// ```rust, no_run
/// use dioxus::prelude::*;
/// use dioxus_desktop::Config;
///
/// fn main() {
/// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default());
/// }
///
/// struct AppProps {
/// name: &'static str
/// }
///
/// fn app(cx: Scope<AppProps>) -> Element {
/// cx.render(rsx!{
/// h1 {"hello {cx.props.name}!"}
/// })
/// }
/// ```
pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config) {
#[cfg(feature = "tokio")]
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(tokio::task::unconstrained(async move {
launch_with_props_blocking(root, props, cfg);
}));
#[cfg(not(feature = "tokio"))]
launch_with_props_blocking(root, props, cfg);
}
/// 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_with_props_blocking<P: 'static>(root: Component<P>, props: P, cfg: Config) {
let (event_loop, mut app) = App::new(cfg, props, root);
event_loop.run(move |window_event, _, control_flow| {
app.tick(&window_event);
match window_event {
Event::NewEvents(StartCause::Init) => app.handle_start_cause_init(),
Event::WindowEvent {
event, window_id, ..
} => match event {
WindowEvent::CloseRequested => app.handle_close_requested(window_id),
WindowEvent::Destroyed { .. } => app.window_destroyed(window_id),
_ => {}
},
Event::UserEvent(UserWindowEvent(event, id)) => match event {
EventData::Poll => app.poll_vdom(id),
EventData::NewWindow => app.handle_new_window(),
EventData::CloseWindow => app.handle_close_msg(id),
EventData::HotReloadEvent(msg) => app.handle_hot_reload_msg(msg),
EventData::Ipc(msg) => match msg.method() {
IpcMethod::FileDialog => app.handle_file_dialog_msg(msg, id),
IpcMethod::UserEvent => app.handle_user_event_msg(msg, id),
IpcMethod::Query => app.handle_query_msg(msg, id),
IpcMethod::BrowserOpen => app.handle_browser_open(msg),
IpcMethod::Initialize => app.handle_initialize_msg(id),
IpcMethod::Other(_) => {}
},
},
_ => {}
}
*control_flow = app.control_flow;
})
}
+28 -456
View File
@@ -3,477 +3,49 @@
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
#![deny(missing_docs)]
mod cfg;
mod app;
mod assets;
mod config;
mod desktop_context;
mod edits;
mod element;
mod escape;
mod eval;
mod events;
mod file_upload;
mod hooks;
mod ipc;
mod menubar;
mod protocol;
mod query;
mod shortcut;
mod waker;
mod webview;
#[cfg(feature = "collect-assets")]
mod collect_assets;
// mobile shortcut is only supported on mobile platforms
#[cfg(any(target_os = "ios", target_os = "android"))]
mod mobile_shortcut;
use crate::query::QueryResult;
pub use cfg::{Config, WindowCloseBehaviour};
pub use desktop_context::DesktopContext;
pub use desktop_context::{
use_window, use_wry_event_handler, DesktopService, WryEventHandler, WryEventHandlerId,
};
use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers};
use dioxus_core::*;
use dioxus_html::MountedData;
use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent};
use element::DesktopElement;
use eval::init_eval;
use futures_util::{pin_mut, FutureExt};
use shortcut::ShortcutRegistry;
pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
use std::cell::Cell;
use std::rc::Rc;
use std::task::Waker;
use std::{collections::HashMap, sync::Arc};
pub use tao::dpi::{LogicalSize, PhysicalSize};
use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
// The main entrypoint for this crate
pub use launch::*;
mod launch;
// Reexport tao and wry, might want to re-export other important things
pub use tao;
pub use tao::dpi::{LogicalPosition, LogicalSize};
pub use tao::event::WindowEvent;
pub use tao::window::WindowBuilder;
use tao::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoop},
};
pub use wry;
pub use wry::application as tao;
use wry::webview::WebView;
use wry::{application::window::WindowId, webview::WebContext};
/// Launch the WebView and run the event loop.
///
/// This function will start a multithreaded Tokio runtime as well the WebView event loop.
///
/// ```rust, ignore
/// use dioxus::prelude::*;
///
/// fn main() {
/// dioxus_desktop::launch(app);
/// }
///
/// fn app(cx: Scope) -> Element {
/// cx.render(rsx!{
/// h1 {"hello world!"}
/// })
/// }
/// ```
pub fn launch(root: Component) {
launch_with_props(root, (), Config::default())
}
/// Launch the WebView and run the event loop, with configuration.
///
/// This function will start a multithreaded Tokio runtime as well the WebView event loop.
///
/// You can configure the WebView window with a configuration closure
///
/// ```rust, ignore
/// use dioxus::prelude::*;
///
/// fn main() {
/// dioxus_desktop::launch_cfg(app, |c| c.with_window(|w| w.with_title("My App")));
/// }
///
/// fn app(cx: Scope) -> Element {
/// cx.render(rsx!{
/// h1 {"hello world!"}
/// })
/// }
/// ```
pub fn launch_cfg(root: Component, config_builder: Config) {
launch_with_props(root, (), config_builder)
}
/// Launch the WebView and run the event loop, with configuration and root props.
///
/// This function will start a multithreaded Tokio runtime as well the WebView event loop. This will block the current thread.
///
/// You can configure the WebView window with a configuration closure
///
/// ```rust, ignore
/// use dioxus::prelude::*;
///
/// fn main() {
/// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default());
/// }
///
/// struct AppProps {
/// name: &'static str
/// }
///
/// fn app(cx: Scope<AppProps>) -> Element {
/// cx.render(rsx!{
/// h1 {"hello {cx.props.name}!"}
/// })
/// }
/// ```
pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config) {
let event_loop = EventLoop::<UserWindowEvent>::with_user_event();
let proxy = event_loop.create_proxy();
let window_behaviour = cfg.last_window_close_behaviour;
// Intialize hot reloading if it is enabled
#[cfg(all(feature = "hot-reload", debug_assertions))]
dioxus_hot_reload::connect({
let proxy = proxy.clone();
move |template| {
let _ = proxy.send_event(UserWindowEvent(
EventData::HotReloadEvent(template),
unsafe { WindowId::dummy() },
));
}
});
// We start the tokio runtime *on this thread*
// Any future we poll later will use this runtime to spawn tasks and for IO
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap();
// We enter the runtime but we poll futures manually, circumventing the per-task runtime budget
let _guard = rt.enter();
// We only have one webview right now, but we'll have more later
// Store them in a hashmap so we can remove them when they're closed
let mut webviews = HashMap::<WindowId, WebviewHandler>::new();
// We use this to allow dynamically adding and removing window event handlers
let event_handlers = WindowEventHandlers::default();
let queue = WebviewQueue::default();
let shortcut_manager = ShortcutRegistry::new(&event_loop);
// move the props into a cell so we can pop it out later to create the first window
// iOS panics if we create a window before the event loop is started
let props = Rc::new(Cell::new(Some(props)));
let cfg = Rc::new(Cell::new(Some(cfg)));
event_loop.run(move |window_event, event_loop, control_flow| {
*control_flow = ControlFlow::Wait;
event_handlers.apply_event(&window_event, event_loop);
match window_event {
Event::WindowEvent {
event, window_id, ..
} => match event {
WindowEvent::CloseRequested => match window_behaviour {
cfg::WindowCloseBehaviour::LastWindowExitsApp => {
webviews.remove(&window_id);
if webviews.is_empty() {
*control_flow = ControlFlow::Exit
}
}
cfg::WindowCloseBehaviour::LastWindowHides => {
let Some(webview) = webviews.get(&window_id) else {
return;
};
hide_app_window(&webview.desktop_context.webview);
}
cfg::WindowCloseBehaviour::CloseWindow => {
webviews.remove(&window_id);
}
},
WindowEvent::Destroyed { .. } => {
webviews.remove(&window_id);
if matches!(
window_behaviour,
cfg::WindowCloseBehaviour::LastWindowExitsApp
) && webviews.is_empty()
{
*control_flow = ControlFlow::Exit
}
}
_ => {}
},
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,
dom,
&queue,
&event_handlers,
shortcut_manager.clone(),
);
let id = handler.desktop_context.webview.window().id();
webviews.insert(id, handler);
_ = proxy.send_event(UserWindowEvent(EventData::Poll, id));
}
Event::UserEvent(UserWindowEvent(EventData::NewWindow, _)) => {
for handler in queue.borrow_mut().drain(..) {
let id = handler.desktop_context.webview.window().id();
webviews.insert(id, handler);
_ = proxy.send_event(UserWindowEvent(EventData::Poll, id));
}
}
Event::UserEvent(event) => match event.0 {
#[cfg(all(feature = "hot-reload", debug_assertions))]
EventData::HotReloadEvent(msg) => match msg {
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
for webview in webviews.values_mut() {
webview.dom.replace_template(template);
poll_vdom(webview);
}
}
dioxus_hot_reload::HotReloadMsg::Shutdown => {
*control_flow = ControlFlow::Exit;
}
},
EventData::CloseWindow => {
webviews.remove(&event.1);
if webviews.is_empty() {
*control_flow = ControlFlow::Exit
}
}
EventData::Poll => {
if let Some(view) = webviews.get_mut(&event.1) {
poll_vdom(view);
}
}
EventData::Ipc(msg) if msg.method() == "user_event" => {
let params = msg.params();
let evt = match serde_json::from_value::<HtmlEvent>(params) {
Ok(value) => value,
Err(_) => return,
};
let HtmlEvent {
element,
name,
bubbles,
data,
} = evt;
let view = webviews.get_mut(&event.1).unwrap();
// check for a mounted event placeholder and replace it with a desktop specific element
let as_any = if let dioxus_html::EventData::Mounted = &data {
let query = view
.dom
.base_scope()
.consume_context::<DesktopContext>()
.unwrap()
.query
.clone();
let element =
DesktopElement::new(element, view.desktop_context.clone(), query);
Rc::new(MountedData::new(element))
} else {
data.into_any()
};
view.dom.handle_event(&name, as_any, element, bubbles);
send_edits(view.dom.render_immediate(), &view.desktop_context.webview);
}
// When the webview sends a query, we need to send it to the query manager which handles dispatching the data to the correct pending query
EventData::Ipc(msg) if msg.method() == "query" => {
let params = msg.params();
if let Ok(result) = serde_json::from_value::<QueryResult>(params) {
let view = webviews.get(&event.1).unwrap();
let query = view
.dom
.base_scope()
.consume_context::<DesktopContext>()
.unwrap()
.query
.clone();
query.send(result);
}
}
EventData::Ipc(msg) if msg.method() == "initialize" => {
let view = webviews.get_mut(&event.1).unwrap();
send_edits(view.dom.rebuild(), &view.desktop_context.webview);
}
EventData::Ipc(msg) if msg.method() == "browser_open" => {
if let Some(temp) = msg.params().as_object() {
if temp.contains_key("href") {
let open = webbrowser::open(temp["href"].as_str().unwrap());
if let Err(e) = open {
tracing::error!("Open Browser error: {:?}", e);
}
}
}
}
EventData::Ipc(msg) if msg.method() == "file_diolog" => {
if let Ok(file_diolog) =
serde_json::from_value::<file_upload::FileDialogRequest>(msg.params())
{
let id = ElementId(file_diolog.target);
let event_name = &file_diolog.event;
let event_bubbles = file_diolog.bubbles;
let files = file_upload::get_file_event(&file_diolog);
let data = Rc::new(FormData {
value: Default::default(),
values: Default::default(),
files: Some(Arc::new(NativeFileEngine::new(files))),
});
let view = webviews.get_mut(&event.1).unwrap();
if event_name == "change&input" {
view.dom
.handle_event("input", data.clone(), id, event_bubbles);
view.dom.handle_event("change", data, id, event_bubbles);
} else {
view.dom.handle_event(event_name, data, id, event_bubbles);
}
send_edits(view.dom.render_immediate(), &view.desktop_context.webview);
}
}
_ => {}
},
Event::GlobalShortcutEvent(id) => shortcut_manager.call_handlers(id),
_ => {}
}
})
}
fn create_new_window(
mut cfg: Config,
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
proxy: &EventLoopProxy<UserWindowEvent>,
dom: VirtualDom,
queue: &WebviewQueue,
event_handlers: &WindowEventHandlers,
shortcut_manager: ShortcutRegistry,
) -> WebviewHandler {
let (webview, web_context) = webview::build(&mut cfg, event_loop, proxy.clone());
let desktop_context = Rc::from(DesktopService::new(
webview,
proxy.clone(),
event_loop.clone(),
queue.clone(),
event_handlers.clone(),
shortcut_manager,
));
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
waker: waker::tao_waker(proxy, desktop_context.webview.window().id()),
desktop_context,
dom,
_web_context: web_context,
}
}
struct WebviewHandler {
dom: VirtualDom,
desktop_context: DesktopContext,
waker: Waker,
// Wry assumes the webcontext is alive for the lifetime of the webview.
// We need to keep the webcontext alive, otherwise the webview will crash
_web_context: WebContext,
}
/// Poll the virtualdom until it's pending
///
/// The waker we give it is connected to the event loop, so it will wake up the event loop when it's ready to be polled again
///
/// All IO is done on the tokio runtime we started earlier
fn poll_vdom(view: &mut WebviewHandler) {
let mut cx = std::task::Context::from_waker(&view.waker);
loop {
{
let fut = view.dom.wait_for_work();
pin_mut!(fut);
match fut.poll_unpin(&mut cx) {
std::task::Poll::Ready(_) => {}
std::task::Poll::Pending => break,
}
}
send_edits(view.dom.render_immediate(), &view.desktop_context.webview);
}
}
/// Send a list of mutations to the webview
fn send_edits(edits: Mutations, webview: &WebView) {
let serialized = serde_json::to_string(&edits).unwrap();
// todo: use SSE and binary data to send the edits with lower overhead
_ = webview.evaluate_script(&format!("window.interpreter.handleEdits({serialized})"));
}
/// Different hide implementations per platform
#[allow(unused)]
fn hide_app_window(webview: &WebView) {
#[cfg(target_os = "windows")]
{
use wry::application::platform::windows::WindowExtWindows;
webview.window().set_visible(false);
webview.window().set_skip_taskbar(true);
}
#[cfg(target_os = "linux")]
{
use wry::application::platform::unix::WindowExtUnix;
webview.window().set_visible(false);
}
#[cfg(target_os = "macos")]
{
// webview.window().set_visible(false); has the wrong behaviour on macOS
// It will hide the window but not show it again when the user switches
// back to the app. `NSApplication::hide:` has the correct behaviour
use objc::runtime::Object;
use objc::{msg_send, sel, sel_impl};
objc::rc::autoreleasepool(|| unsafe {
let app: *mut Object = msg_send![objc::class!(NSApplication), sharedApplication];
let nil = std::ptr::null_mut::<Object>();
let _: () = msg_send![app, hide: nil];
});
}
}
// Public exports
pub use assets::AssetRequest;
pub use config::{Config, WindowCloseBehaviour};
pub use desktop_context::{
window, DesktopContext, DesktopService, WryEventHandler, WryEventHandlerId,
};
pub use hooks::{use_asset_handler, use_global_shortcut, use_window, use_wry_event_handler};
pub use shortcut::{ShortcutHandle, ShortcutId, ShortcutRegistryError};
pub use wry::RequestAsyncResponder;
+79
View File
@@ -0,0 +1,79 @@
use tao::window::Window;
#[allow(unused)]
pub fn build_menu(window: &Window, default_menu_bar: bool) {
if default_menu_bar {
#[cfg(not(any(target_os = "ios", target_os = "android")))]
impl_::build_menu_bar(impl_::build_default_menu_bar(), window)
}
}
#[cfg(not(any(target_os = "ios", target_os = "android")))]
mod impl_ {
use super::*;
use muda::{Menu, PredefinedMenuItem, Submenu};
#[allow(unused)]
pub fn build_menu_bar(menu: Menu, window: &Window) {
#[cfg(target_os = "windows")]
use tao::platform::windows::WindowExtWindows;
#[cfg(target_os = "windows")]
menu.init_for_hwnd(window.hwnd());
// #[cfg(target_os = "linux")]
// {
// use tao::platform::unix::WindowExtUnix;
// menu.init_for_gtk_window(window, None);
// menu.init_for_gtk_window(window, Some(&ertical_gtk_box));
// }
#[cfg(target_os = "macos")]
menu.init_for_nsapp();
}
/// Builds a standard menu bar depending on the users platform. It may be used as a starting point
/// to further customize the menu bar and pass it to a [`WindowBuilder`](tao::window::WindowBuilder).
/// > Note: The default menu bar enables macOS shortcuts like cut/copy/paste.
/// > The menu bar differs per platform because of constraints introduced
/// > by [`MenuItem`](tao::menu::MenuItem).
pub fn build_default_menu_bar() -> Menu {
let menu = Menu::new();
// since it is uncommon on windows to have an "application menu"
// we add a "window" menu to be more consistent across platforms with the standard menu
let window_menu = Submenu::new("Window", true);
window_menu
.append_items(&[
&PredefinedMenuItem::fullscreen(None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::hide(None),
&PredefinedMenuItem::hide_others(None),
&PredefinedMenuItem::show_all(None),
&PredefinedMenuItem::maximize(None),
&PredefinedMenuItem::minimize(None),
&PredefinedMenuItem::close_window(None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::quit(None),
])
.unwrap();
let edit_menu = Submenu::new("Window", true);
edit_menu
.append_items(&[
&PredefinedMenuItem::undo(None),
&PredefinedMenuItem::redo(None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::cut(None),
&PredefinedMenuItem::copy(None),
&PredefinedMenuItem::paste(None),
&PredefinedMenuItem::separator(),
&PredefinedMenuItem::select_all(None),
])
.unwrap();
menu.append_items(&[&window_menu, &edit_menu]).unwrap();
menu
}
}
+55 -21
View File
@@ -1,29 +1,51 @@
#![allow(unused)]
use super::*;
use wry::application::accelerator::Accelerator;
use wry::application::event_loop::EventLoopWindowTarget;
use std::str::FromStr;
use tao::event_loop::EventLoopWindowTarget;
pub struct GlobalShortcut();
pub struct ShortcutManager();
use dioxus_html::input_data::keyboard_types::Modifiers;
impl ShortcutManager {
pub fn new<T>(target: &EventLoopWindowTarget<T>) -> Self {
Self()
#[derive(Clone, Debug)]
pub struct Accelerator;
#[derive(Clone, Copy)]
pub struct HotKey;
impl HotKey {
pub fn new(mods: Option<Modifiers>, key: Code) -> Self {
Self
}
pub fn register(
&mut self,
accelerator: Accelerator,
) -> Result<GlobalShortcut, ShortcutManagerError> {
Ok(GlobalShortcut())
pub fn id(&self) -> u32 {
0
}
}
impl FromStr for HotKey {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(HotKey)
}
}
pub struct GlobalHotKeyManager();
impl GlobalHotKeyManager {
pub fn new() -> Result<Self, HotkeyError> {
Ok(Self())
}
pub fn unregister(&mut self, id: ShortcutId) -> Result<(), ShortcutManagerError> {
pub fn register(&self, accelerator: HotKey) -> Result<HotKey, HotkeyError> {
Ok(HotKey)
}
pub fn unregister(&self, id: HotKey) -> Result<(), HotkeyError> {
Ok(())
}
pub fn unregister_all(&mut self) -> Result<(), ShortcutManagerError> {
pub fn unregister_all(&self, _: &[HotKey]) -> Result<(), HotkeyError> {
Ok(())
}
}
@@ -33,23 +55,35 @@ use std::{error, fmt};
/// An error whose cause the `ShortcutManager` to fail.
#[non_exhaustive]
#[derive(Debug)]
pub enum ShortcutManagerError {
pub enum HotkeyError {
AcceleratorAlreadyRegistered(Accelerator),
AcceleratorNotRegistered(Accelerator),
InvalidAccelerator(String),
HotKeyParseError(String),
}
impl error::Error for ShortcutManagerError {}
impl fmt::Display for ShortcutManagerError {
impl error::Error for HotkeyError {}
impl fmt::Display for HotkeyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self {
ShortcutManagerError::AcceleratorAlreadyRegistered(e) => {
HotkeyError::AcceleratorAlreadyRegistered(e) => {
f.pad(&format!("hotkey already registered: {:?}", e))
}
ShortcutManagerError::AcceleratorNotRegistered(e) => {
HotkeyError::AcceleratorNotRegistered(e) => {
f.pad(&format!("hotkey not registered: {:?}", e))
}
ShortcutManagerError::InvalidAccelerator(e) => e.fmt(f),
HotkeyError::HotKeyParseError(e) => e.fmt(f),
}
}
}
pub struct GlobalHotKeyEvent {
pub id: u32,
}
impl GlobalHotKeyEvent {
pub fn receiver() -> crossbeam_channel::Receiver<GlobalHotKeyEvent> {
crossbeam_channel::unbounded().1
}
}
pub(crate) type Code = dioxus_html::input_data::keyboard_types::Code;
+179 -134
View File
@@ -1,151 +1,204 @@
use dioxus_interpreter_js::{COMMON_JS, INTERPRETER_JS};
use std::{
borrow::Cow,
path::{Path, PathBuf},
};
use crate::{assets::*, edits::EditQueue};
use std::path::{Path, PathBuf};
use wry::{
http::{status::StatusCode, Request, Response},
Result,
http::{status::StatusCode, Request, Response, Uri},
RequestAsyncResponder, Result,
};
fn module_loader(root_name: &str) -> String {
let js = INTERPRETER_JS.replace(
"/*POST_HANDLE_EDITS*/",
r#"// Prevent file inputs from opening the file dialog on click
let inputs = document.querySelectorAll("input");
for (let input of inputs) {
if (!input.getAttribute("data-dioxus-file-listener")) {
// prevent file inputs from opening the file dialog on click
const type = input.getAttribute("type");
if (type === "file") {
input.setAttribute("data-dioxus-file-listener", true);
input.addEventListener("click", (event) => {
let target = event.target;
let target_id = find_real_id(target);
if (target_id !== null) {
const send = (event_name) => {
const message = serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name });
window.ipc.postMessage(message);
};
send("change&input");
}
event.preventDefault();
});
}
}
}"#,
static MINIFIED: &str = include_str!("./minified.js");
static DEFAULT_INDEX: &str = include_str!("./index.html");
/// Build the index.html file we use for bootstrapping a new app
///
/// We use wry/webview by building a special index.html that forms a bridge between the webview and your rust code
///
/// This is similar to tauri, except we give more power to your rust code and less power to your frontend code.
/// This lets us skip a build/bundle step - your code just works - but limits how your Rust code can actually
/// mess with UI elements. We make this decision since other renderers like LiveView are very separate and can
/// never properly bridge the gap. Eventually of course, the idea is to build a custom CSS/HTML renderer where you
/// *do* have native control over elements, but that still won't work with liveview.
pub(super) fn index_request(
request: &Request<Vec<u8>>,
custom_head: Option<String>,
custom_index: Option<String>,
root_name: &str,
headless: bool,
) -> Option<Response<Vec<u8>>> {
// If the request is for the root, we'll serve the index.html file.
if request.uri().path() != "/" {
return None;
}
// Load a custom index file if provided
let mut index = custom_index.unwrap_or_else(|| DEFAULT_INDEX.to_string());
// Insert a custom head if provided
// We look just for the closing head tag. If a user provided a custom index with weird syntax, this might fail
if let Some(head) = custom_head {
index.insert_str(index.find("</head>").expect("Head element to exist"), &head);
}
// Inject our module loader by looking for a body tag
// A failure mode here, obviously, is if the user provided a custom index without a body tag
// Might want to document this
index.insert_str(
index.find("</body>").expect("Body element to exist"),
&module_loader(root_name, headless),
);
Response::builder()
.header("Content-Type", "text/html")
.header("Access-Control-Allow-Origin", "*")
.body(index.into())
.ok()
}
/// Handle a request from the webview
///
/// - Tries to stream edits if they're requested.
/// - If that doesn't match, tries a user provided asset handler
/// - If that doesn't match, tries to serve a file from the filesystem
pub(super) fn desktop_handler(
mut request: Request<Vec<u8>>,
asset_handlers: AssetHandlerRegistry,
edit_queue: &EditQueue,
responder: RequestAsyncResponder,
) {
// If the request is asking for edits (ie binary protocol streaming, do that)
if request.uri().path().trim_matches('/') == "edits" {
return edit_queue.handle_request(responder);
}
// If the user provided a custom asset handler, then call it and return the response if the request was handled.
// The path is the first part of the URI, so we need to trim the leading slash.
let path = PathBuf::from(
urlencoding::decode(request.uri().path().trim_start_matches('/'))
.expect("expected URL to be UTF-8 encoded")
.as_ref(),
);
let Some(name) = path.parent() else {
return tracing::error!("Asset request has no root {path:?}");
};
if let Some(name) = name.to_str() {
if asset_handlers.has_handler(name) {
// Trim the leading path from the URI
//
// I hope this is reliable!
//
// so a request for /assets/logos/logo.png?query=123 will become /logos/logo.png?query=123
strip_uri_prefix(&mut request, name);
return asset_handlers.handle_request(name, request, responder);
}
}
// Else, try to serve a file from the filesystem.
match serve_from_fs(path) {
Ok(res) => responder.respond(res),
Err(e) => tracing::error!("Error serving request from filesystem {}", e),
}
}
fn serve_from_fs(path: PathBuf) -> Result<Response<Vec<u8>>> {
// If the path is relative, we'll try to serve it from the assets directory.
let mut asset = get_asset_root_or_default().join(&path);
// If we can't find it, make it absolute and try again
if !asset.exists() {
asset = PathBuf::from("/").join(path);
}
if !asset.exists() {
return Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(String::from("Not Found").into_bytes())?);
}
Ok(Response::builder()
.header("Content-Type", get_mime_from_path(&asset)?)
.body(std::fs::read(asset)?)?)
}
fn strip_uri_prefix(request: &mut Request<Vec<u8>>, name: &str) {
// trim the leading path
if let Some(path) = request.uri().path_and_query() {
let new_path = path
.path()
.trim_start_matches('/')
.strip_prefix(name)
.expect("expected path to have prefix");
let new_uri = Uri::builder()
.scheme(request.uri().scheme_str().unwrap_or("http"))
.path_and_query(format!("{}{}", new_path, path.query().unwrap_or("")))
.authority("index.html")
.build()
.expect("failed to build new URI");
*request.uri_mut() = new_uri;
}
}
/// Construct the inline script that boots up the page and bridges the webview with rust code.
///
/// The arguments here:
/// - 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 {
format!(
r#"
<script type="module">
{js}
let rootname = "{root_name}";
let root = window.document.getElementById(rootname);
if (root != null) {{
window.interpreter = new Interpreter(root, new InterpreterConfig(true));
window.ipc.postMessage(serializeIpcMessage("initialize"));
{MINIFIED}
// Wait for the page to load
window.onload = function() {{
let rootname = "{root_id}";
let root_element = window.document.getElementById(rootname);
if (root_element != null) {{
window.interpreter.initialize(root_element);
window.ipc.postMessage(window.interpreter.serializeIpcMessage("initialize"));
}}
window.interpreter.wait_for_request({headless});
}}
</script>
"#
)
}
pub(super) fn desktop_handler(
request: &Request<Vec<u8>>,
custom_head: Option<String>,
custom_index: Option<String>,
root_name: &str,
) -> Result<Response<Cow<'static, [u8]>>> {
// If the request is for the root, we'll serve the index.html file.
if request.uri().path() == "/" {
// If a custom index is provided, just defer to that, expecting the user to know what they're doing.
// we'll look for the closing </body> tag and insert our little module loader there.
let body = match custom_index {
Some(custom_index) => custom_index
.replace("</body>", &format!("{}</body>", module_loader(root_name)))
.into_bytes(),
None => {
// Otherwise, we'll serve the default index.html and apply a custom head if that's specified.
let mut template = include_str!("./index.html").to_string();
if let Some(custom_head) = custom_head {
template = template.replace("<!-- CUSTOM HEAD -->", &custom_head);
}
template
.replace("<!-- MODULE LOADER -->", &module_loader(root_name))
.into_bytes()
}
};
return Response::builder()
.header("Content-Type", "text/html")
.body(Cow::from(body))
.map_err(From::from);
} else if request.uri().path() == "/common.js" {
return Response::builder()
.header("Content-Type", "text/javascript")
.body(Cow::from(COMMON_JS.as_bytes()))
.map_err(From::from);
}
// Else, try to serve a file from the filesystem.
let decoded = urlencoding::decode(request.uri().path().trim_start_matches('/'))
.expect("expected URL to be UTF-8 encoded");
let path = PathBuf::from(&*decoded);
// If the path is relative, we'll try to serve it from the assets directory.
let mut asset = get_asset_root()
.unwrap_or_else(|| Path::new(".").to_path_buf())
.join(&path);
if !asset.exists() {
asset = PathBuf::from("/").join(path);
}
if asset.exists() {
return Response::builder()
.header("Content-Type", get_mime_from_path(&asset)?)
.body(Cow::from(std::fs::read(asset)?))
.map_err(From::from);
}
Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Cow::from(String::from("Not Found").into_bytes()))
.map_err(From::from)
/// Get the assset directory, following tauri/cargo-bundles directory discovery approach
///
/// Defaults to the current directory if no asset directory is found, which is useful for development when the app
/// isn't bundled.
fn get_asset_root_or_default() -> PathBuf {
get_asset_root().unwrap_or_else(|| Path::new(".").to_path_buf())
}
/// Get the asset directory, following tauri/cargo-bundles directory discovery approach
///
/// Currently supports:
/// - [x] macOS
/// - [ ] Windows
/// - [ ] Linux (rpm)
/// - [ ] Linux (deb)
/// - [ ] iOS
/// - [ ] Android
#[allow(unreachable_code)]
fn get_asset_root() -> Option<PathBuf> {
/*
We're matching exactly how cargo-bundle works.
- [x] macOS
- [ ] Windows
- [ ] Linux (rpm)
- [ ] Linux (deb)
- [ ] iOS
- [ ] Android
*/
// If running under cargo, there's no bundle!
// There might be a smarter/more resilient way of doing this
if std::env::var_os("CARGO").is_some() {
return None;
}
// TODO: support for other platforms
#[cfg(target_os = "macos")]
{
let bundle = core_foundation::bundle::CFBundle::main_bundle();
let bundle_path = bundle.path()?;
let resources_path = bundle.resources_path()?;
let absolute_resources_root = bundle_path.join(resources_path);
let canonical_resources_root = dunce::canonicalize(absolute_resources_root).ok()?;
return Some(canonical_resources_root);
return dunce::canonicalize(absolute_resources_root).ok();
}
None
@@ -153,22 +206,14 @@ fn get_asset_root() -> Option<PathBuf> {
/// Get the mime type from a path-like string
fn get_mime_from_path(trimmed: &Path) -> Result<&'static str> {
if trimmed.ends_with(".svg") {
if trimmed.extension().is_some_and(|ext| ext == "svg") {
return Ok("image/svg+xml");
}
let res = match infer::get_from_path(trimmed)?.map(|f| f.mime_type()) {
Some(f) => {
if f == "text/plain" {
get_mime_by_ext(trimmed)
} else {
f
}
}
None => get_mime_by_ext(trimmed),
};
Ok(res)
match infer::get_from_path(trimmed)?.map(|f| f.mime_type()) {
Some(f) if f != "text/plain" => Ok(f),
_ => Ok(get_mime_by_ext(trimmed)),
}
}
/// Get the mime type from a URI using its extension
+179 -211
View File
@@ -1,15 +1,10 @@
use std::{cell::RefCell, collections::HashMap, rc::Rc, str::FromStr};
use std::{cell::RefCell, collections::HashMap, str::FromStr};
use dioxus_core::ScopeState;
use dioxus_html::input_data::keyboard_types::Modifiers;
use slab::Slab;
use wry::application::{
accelerator::{Accelerator, AcceleratorId},
event_loop::EventLoopWindowTarget,
keyboard::{KeyCode, ModifiersState},
};
use tao::keyboard::ModifiersState;
use crate::{desktop_context::DesktopContext, use_window};
use crate::desktop_context::DesktopContext;
#[cfg(any(
target_os = "windows",
@@ -20,22 +15,22 @@ use crate::{desktop_context::DesktopContext, use_window};
target_os = "netbsd",
target_os = "openbsd"
))]
use wry::application::global_shortcut::{GlobalShortcut, ShortcutManager, ShortcutManagerError};
pub use global_hotkey::{
hotkey::{Code, HotKey},
Error as HotkeyError, GlobalHotKeyEvent, GlobalHotKeyManager,
};
#[cfg(any(target_os = "ios", target_os = "android"))]
pub use crate::mobile_shortcut::*;
#[derive(Clone)]
pub(crate) struct ShortcutRegistry {
manager: Rc<RefCell<ShortcutManager>>,
shortcuts: ShortcutMap,
manager: GlobalHotKeyManager,
shortcuts: RefCell<HashMap<u32, Shortcut>>,
}
type ShortcutMap = Rc<RefCell<HashMap<AcceleratorId, Shortcut>>>;
struct Shortcut {
#[allow(unused)]
shortcut: GlobalShortcut,
shortcut: HotKey,
callbacks: Slab<Box<dyn FnMut()>>,
}
@@ -54,15 +49,15 @@ impl Shortcut {
}
impl ShortcutRegistry {
pub fn new<T>(target: &EventLoopWindowTarget<T>) -> Self {
pub fn new() -> Self {
Self {
manager: Rc::new(RefCell::new(ShortcutManager::new(target))),
shortcuts: Rc::new(RefCell::new(HashMap::new())),
manager: GlobalHotKeyManager::new().unwrap(),
shortcuts: RefCell::new(HashMap::new()),
}
}
pub(crate) fn call_handlers(&self, id: AcceleratorId) {
if let Some(Shortcut { callbacks, .. }) = self.shortcuts.borrow_mut().get_mut(&id) {
pub(crate) fn call_handlers(&self, id: GlobalHotKeyEvent) {
if let Some(Shortcut { callbacks, .. }) = self.shortcuts.borrow_mut().get_mut(&id.id) {
for (_, callback) in callbacks.iter_mut() {
(callback)();
}
@@ -71,40 +66,40 @@ impl ShortcutRegistry {
pub(crate) fn add_shortcut(
&self,
accelerator: Accelerator,
hotkey: HotKey,
callback: Box<dyn FnMut()>,
) -> Result<ShortcutId, ShortcutRegistryError> {
let accelerator_id = accelerator.clone().id();
let accelerator_id = hotkey.clone().id();
let mut shortcuts = self.shortcuts.borrow_mut();
Ok(
if let Some(callbacks) = shortcuts.get_mut(&accelerator_id) {
let id = callbacks.insert(callback);
ShortcutId {
id: accelerator_id,
number: id,
}
} else {
match self.manager.borrow_mut().register(accelerator) {
Ok(global_shortcut) => {
let mut slab = Slab::new();
let id = slab.insert(callback);
let shortcut = Shortcut {
shortcut: global_shortcut,
callbacks: slab,
};
shortcuts.insert(accelerator_id, shortcut);
ShortcutId {
id: accelerator_id,
number: id,
}
}
Err(ShortcutManagerError::InvalidAccelerator(shortcut)) => {
return Err(ShortcutRegistryError::InvalidShortcut(shortcut))
}
Err(err) => return Err(ShortcutRegistryError::Other(Box::new(err))),
}
},
)
if let Some(callbacks) = shortcuts.get_mut(&accelerator_id) {
return Ok(ShortcutId {
id: accelerator_id,
number: callbacks.insert(callback),
});
};
self.manager.register(hotkey).map_err(|e| match e {
HotkeyError::HotKeyParseError(shortcut) => {
ShortcutRegistryError::InvalidShortcut(shortcut)
}
err => ShortcutRegistryError::Other(Box::new(err)),
})?;
let mut shortcut = Shortcut {
shortcut: hotkey,
callbacks: Slab::new(),
};
let id = shortcut.callbacks.insert(callback);
shortcuts.insert(accelerator_id, shortcut);
Ok(ShortcutId {
id: accelerator_id,
number: id,
})
}
pub(crate) fn remove_shortcut(&self, id: ShortcutId) {
@@ -113,16 +108,7 @@ impl ShortcutRegistry {
callbacks.remove(id.number);
if callbacks.is_empty() {
if let Some(_shortcut) = shortcuts.remove(&id.id) {
#[cfg(any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
let _ = self.manager.borrow_mut().unregister(_shortcut.shortcut);
let _ = self.manager.unregister(_shortcut.shortcut);
}
}
}
@@ -130,8 +116,8 @@ impl ShortcutRegistry {
pub(crate) fn remove_all(&self) {
let mut shortcuts = self.shortcuts.borrow_mut();
shortcuts.clear();
let _ = self.manager.borrow_mut().unregister_all();
let hotkeys: Vec<_> = shortcuts.drain().map(|(_, v)| v.shortcut).collect();
let _ = self.manager.unregister_all(&hotkeys);
}
}
@@ -148,64 +134,45 @@ pub enum ShortcutRegistryError {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
/// An global id for a shortcut.
pub struct ShortcutId {
id: AcceleratorId,
id: u32,
number: usize,
}
/// A global shortcut. This will be automatically removed when it is dropped.
pub struct ShortcutHandle {
desktop: DesktopContext,
pub(crate) desktop: DesktopContext,
/// The id of the shortcut
pub shortcut_id: ShortcutId,
}
pub trait IntoAccelerator {
fn accelerator(&self) -> Accelerator;
fn accelerator(&self) -> HotKey;
}
impl IntoAccelerator for (dioxus_html::KeyCode, ModifiersState) {
fn accelerator(&self) -> Accelerator {
Accelerator::new(Some(self.1), self.0.into_key_code())
fn accelerator(&self) -> HotKey {
HotKey::new(Some(self.1.into_modifiers_state()), self.0.into_key_code())
}
}
impl IntoAccelerator for (ModifiersState, dioxus_html::KeyCode) {
fn accelerator(&self) -> Accelerator {
Accelerator::new(Some(self.0), self.1.into_key_code())
fn accelerator(&self) -> HotKey {
HotKey::new(Some(self.0.into_modifiers_state()), self.1.into_key_code())
}
}
impl IntoAccelerator for dioxus_html::KeyCode {
fn accelerator(&self) -> Accelerator {
Accelerator::new(None, self.into_key_code())
fn accelerator(&self) -> HotKey {
HotKey::new(None, self.into_key_code())
}
}
impl IntoAccelerator for &str {
fn accelerator(&self) -> Accelerator {
Accelerator::from_str(self).unwrap()
fn accelerator(&self) -> HotKey {
HotKey::from_str(self).unwrap()
}
}
/// Get a closure that executes any JavaScript in the WebView context.
pub fn use_global_shortcut(
cx: &ScopeState,
accelerator: impl IntoAccelerator,
handler: impl FnMut() + 'static,
) -> &Result<ShortcutHandle, ShortcutRegistryError> {
let desktop = use_window(cx);
cx.use_hook(move || {
let desktop = desktop.clone();
let id = desktop.create_shortcut(accelerator.accelerator(), handler);
Ok(ShortcutHandle {
desktop,
shortcut_id: id?,
})
})
}
impl ShortcutHandle {
/// Remove the shortcut.
pub fn remove(&self) {
@@ -220,143 +187,144 @@ impl Drop for ShortcutHandle {
}
pub trait IntoModifersState {
fn into_modifiers_state(self) -> ModifiersState;
fn into_modifiers_state(self) -> Modifiers;
}
impl IntoModifersState for ModifiersState {
fn into_modifiers_state(self) -> ModifiersState {
self
fn into_modifiers_state(self) -> Modifiers {
let mut modifiers = Modifiers::default();
if self.shift_key() {
modifiers |= Modifiers::SHIFT;
}
if self.control_key() {
modifiers |= Modifiers::CONTROL;
}
if self.alt_key() {
modifiers |= Modifiers::ALT;
}
if self.super_key() {
modifiers |= Modifiers::META;
}
modifiers
}
}
impl IntoModifersState for Modifiers {
fn into_modifiers_state(self) -> ModifiersState {
let mut state = ModifiersState::empty();
if self.contains(Modifiers::SHIFT) {
state |= ModifiersState::SHIFT
}
if self.contains(Modifiers::CONTROL) {
state |= ModifiersState::CONTROL
}
if self.contains(Modifiers::ALT) {
state |= ModifiersState::ALT
}
if self.contains(Modifiers::META) || self.contains(Modifiers::SUPER) {
state |= ModifiersState::SUPER
}
state
fn into_modifiers_state(self) -> Modifiers {
self
}
}
pub trait IntoKeyCode {
fn into_key_code(self) -> KeyCode;
fn into_key_code(self) -> Code;
}
impl IntoKeyCode for KeyCode {
fn into_key_code(self) -> KeyCode {
impl IntoKeyCode for Code {
fn into_key_code(self) -> Code {
self
}
}
impl IntoKeyCode for dioxus_html::KeyCode {
fn into_key_code(self) -> KeyCode {
fn into_key_code(self) -> Code {
match self {
dioxus_html::KeyCode::Backspace => KeyCode::Backspace,
dioxus_html::KeyCode::Tab => KeyCode::Tab,
dioxus_html::KeyCode::Clear => KeyCode::NumpadClear,
dioxus_html::KeyCode::Enter => KeyCode::Enter,
dioxus_html::KeyCode::Shift => KeyCode::ShiftLeft,
dioxus_html::KeyCode::Ctrl => KeyCode::ControlLeft,
dioxus_html::KeyCode::Alt => KeyCode::AltLeft,
dioxus_html::KeyCode::Pause => KeyCode::Pause,
dioxus_html::KeyCode::CapsLock => KeyCode::CapsLock,
dioxus_html::KeyCode::Escape => KeyCode::Escape,
dioxus_html::KeyCode::Space => KeyCode::Space,
dioxus_html::KeyCode::PageUp => KeyCode::PageUp,
dioxus_html::KeyCode::PageDown => KeyCode::PageDown,
dioxus_html::KeyCode::End => KeyCode::End,
dioxus_html::KeyCode::Home => KeyCode::Home,
dioxus_html::KeyCode::LeftArrow => KeyCode::ArrowLeft,
dioxus_html::KeyCode::UpArrow => KeyCode::ArrowUp,
dioxus_html::KeyCode::RightArrow => KeyCode::ArrowRight,
dioxus_html::KeyCode::DownArrow => KeyCode::ArrowDown,
dioxus_html::KeyCode::Insert => KeyCode::Insert,
dioxus_html::KeyCode::Delete => KeyCode::Delete,
dioxus_html::KeyCode::Num0 => KeyCode::Numpad0,
dioxus_html::KeyCode::Num1 => KeyCode::Numpad1,
dioxus_html::KeyCode::Num2 => KeyCode::Numpad2,
dioxus_html::KeyCode::Num3 => KeyCode::Numpad3,
dioxus_html::KeyCode::Num4 => KeyCode::Numpad4,
dioxus_html::KeyCode::Num5 => KeyCode::Numpad5,
dioxus_html::KeyCode::Num6 => KeyCode::Numpad6,
dioxus_html::KeyCode::Num7 => KeyCode::Numpad7,
dioxus_html::KeyCode::Num8 => KeyCode::Numpad8,
dioxus_html::KeyCode::Num9 => KeyCode::Numpad9,
dioxus_html::KeyCode::A => KeyCode::KeyA,
dioxus_html::KeyCode::B => KeyCode::KeyB,
dioxus_html::KeyCode::C => KeyCode::KeyC,
dioxus_html::KeyCode::D => KeyCode::KeyD,
dioxus_html::KeyCode::E => KeyCode::KeyE,
dioxus_html::KeyCode::F => KeyCode::KeyF,
dioxus_html::KeyCode::G => KeyCode::KeyG,
dioxus_html::KeyCode::H => KeyCode::KeyH,
dioxus_html::KeyCode::I => KeyCode::KeyI,
dioxus_html::KeyCode::J => KeyCode::KeyJ,
dioxus_html::KeyCode::K => KeyCode::KeyK,
dioxus_html::KeyCode::L => KeyCode::KeyL,
dioxus_html::KeyCode::M => KeyCode::KeyM,
dioxus_html::KeyCode::N => KeyCode::KeyN,
dioxus_html::KeyCode::O => KeyCode::KeyO,
dioxus_html::KeyCode::P => KeyCode::KeyP,
dioxus_html::KeyCode::Q => KeyCode::KeyQ,
dioxus_html::KeyCode::R => KeyCode::KeyR,
dioxus_html::KeyCode::S => KeyCode::KeyS,
dioxus_html::KeyCode::T => KeyCode::KeyT,
dioxus_html::KeyCode::U => KeyCode::KeyU,
dioxus_html::KeyCode::V => KeyCode::KeyV,
dioxus_html::KeyCode::W => KeyCode::KeyW,
dioxus_html::KeyCode::X => KeyCode::KeyX,
dioxus_html::KeyCode::Y => KeyCode::KeyY,
dioxus_html::KeyCode::Z => KeyCode::KeyZ,
dioxus_html::KeyCode::Numpad0 => KeyCode::Numpad0,
dioxus_html::KeyCode::Numpad1 => KeyCode::Numpad1,
dioxus_html::KeyCode::Numpad2 => KeyCode::Numpad2,
dioxus_html::KeyCode::Numpad3 => KeyCode::Numpad3,
dioxus_html::KeyCode::Numpad4 => KeyCode::Numpad4,
dioxus_html::KeyCode::Numpad5 => KeyCode::Numpad5,
dioxus_html::KeyCode::Numpad6 => KeyCode::Numpad6,
dioxus_html::KeyCode::Numpad7 => KeyCode::Numpad7,
dioxus_html::KeyCode::Numpad8 => KeyCode::Numpad8,
dioxus_html::KeyCode::Numpad9 => KeyCode::Numpad9,
dioxus_html::KeyCode::Multiply => KeyCode::NumpadMultiply,
dioxus_html::KeyCode::Add => KeyCode::NumpadAdd,
dioxus_html::KeyCode::Subtract => KeyCode::NumpadSubtract,
dioxus_html::KeyCode::DecimalPoint => KeyCode::NumpadDecimal,
dioxus_html::KeyCode::Divide => KeyCode::NumpadDivide,
dioxus_html::KeyCode::F1 => KeyCode::F1,
dioxus_html::KeyCode::F2 => KeyCode::F2,
dioxus_html::KeyCode::F3 => KeyCode::F3,
dioxus_html::KeyCode::F4 => KeyCode::F4,
dioxus_html::KeyCode::F5 => KeyCode::F5,
dioxus_html::KeyCode::F6 => KeyCode::F6,
dioxus_html::KeyCode::F7 => KeyCode::F7,
dioxus_html::KeyCode::F8 => KeyCode::F8,
dioxus_html::KeyCode::F9 => KeyCode::F9,
dioxus_html::KeyCode::F10 => KeyCode::F10,
dioxus_html::KeyCode::F11 => KeyCode::F11,
dioxus_html::KeyCode::F12 => KeyCode::F12,
dioxus_html::KeyCode::NumLock => KeyCode::NumLock,
dioxus_html::KeyCode::ScrollLock => KeyCode::ScrollLock,
dioxus_html::KeyCode::Semicolon => KeyCode::Semicolon,
dioxus_html::KeyCode::EqualSign => KeyCode::Equal,
dioxus_html::KeyCode::Comma => KeyCode::Comma,
dioxus_html::KeyCode::Period => KeyCode::Period,
dioxus_html::KeyCode::ForwardSlash => KeyCode::Slash,
dioxus_html::KeyCode::GraveAccent => KeyCode::Backquote,
dioxus_html::KeyCode::OpenBracket => KeyCode::BracketLeft,
dioxus_html::KeyCode::BackSlash => KeyCode::Backslash,
dioxus_html::KeyCode::CloseBraket => KeyCode::BracketRight,
dioxus_html::KeyCode::SingleQuote => KeyCode::Quote,
dioxus_html::KeyCode::Backspace => Code::Backspace,
dioxus_html::KeyCode::Tab => Code::Tab,
dioxus_html::KeyCode::Clear => Code::NumpadClear,
dioxus_html::KeyCode::Enter => Code::Enter,
dioxus_html::KeyCode::Shift => Code::ShiftLeft,
dioxus_html::KeyCode::Ctrl => Code::ControlLeft,
dioxus_html::KeyCode::Alt => Code::AltLeft,
dioxus_html::KeyCode::Pause => Code::Pause,
dioxus_html::KeyCode::CapsLock => Code::CapsLock,
dioxus_html::KeyCode::Escape => Code::Escape,
dioxus_html::KeyCode::Space => Code::Space,
dioxus_html::KeyCode::PageUp => Code::PageUp,
dioxus_html::KeyCode::PageDown => Code::PageDown,
dioxus_html::KeyCode::End => Code::End,
dioxus_html::KeyCode::Home => Code::Home,
dioxus_html::KeyCode::LeftArrow => Code::ArrowLeft,
dioxus_html::KeyCode::UpArrow => Code::ArrowUp,
dioxus_html::KeyCode::RightArrow => Code::ArrowRight,
dioxus_html::KeyCode::DownArrow => Code::ArrowDown,
dioxus_html::KeyCode::Insert => Code::Insert,
dioxus_html::KeyCode::Delete => Code::Delete,
dioxus_html::KeyCode::Num0 => Code::Numpad0,
dioxus_html::KeyCode::Num1 => Code::Numpad1,
dioxus_html::KeyCode::Num2 => Code::Numpad2,
dioxus_html::KeyCode::Num3 => Code::Numpad3,
dioxus_html::KeyCode::Num4 => Code::Numpad4,
dioxus_html::KeyCode::Num5 => Code::Numpad5,
dioxus_html::KeyCode::Num6 => Code::Numpad6,
dioxus_html::KeyCode::Num7 => Code::Numpad7,
dioxus_html::KeyCode::Num8 => Code::Numpad8,
dioxus_html::KeyCode::Num9 => Code::Numpad9,
dioxus_html::KeyCode::A => Code::KeyA,
dioxus_html::KeyCode::B => Code::KeyB,
dioxus_html::KeyCode::C => Code::KeyC,
dioxus_html::KeyCode::D => Code::KeyD,
dioxus_html::KeyCode::E => Code::KeyE,
dioxus_html::KeyCode::F => Code::KeyF,
dioxus_html::KeyCode::G => Code::KeyG,
dioxus_html::KeyCode::H => Code::KeyH,
dioxus_html::KeyCode::I => Code::KeyI,
dioxus_html::KeyCode::J => Code::KeyJ,
dioxus_html::KeyCode::K => Code::KeyK,
dioxus_html::KeyCode::L => Code::KeyL,
dioxus_html::KeyCode::M => Code::KeyM,
dioxus_html::KeyCode::N => Code::KeyN,
dioxus_html::KeyCode::O => Code::KeyO,
dioxus_html::KeyCode::P => Code::KeyP,
dioxus_html::KeyCode::Q => Code::KeyQ,
dioxus_html::KeyCode::R => Code::KeyR,
dioxus_html::KeyCode::S => Code::KeyS,
dioxus_html::KeyCode::T => Code::KeyT,
dioxus_html::KeyCode::U => Code::KeyU,
dioxus_html::KeyCode::V => Code::KeyV,
dioxus_html::KeyCode::W => Code::KeyW,
dioxus_html::KeyCode::X => Code::KeyX,
dioxus_html::KeyCode::Y => Code::KeyY,
dioxus_html::KeyCode::Z => Code::KeyZ,
dioxus_html::KeyCode::Numpad0 => Code::Numpad0,
dioxus_html::KeyCode::Numpad1 => Code::Numpad1,
dioxus_html::KeyCode::Numpad2 => Code::Numpad2,
dioxus_html::KeyCode::Numpad3 => Code::Numpad3,
dioxus_html::KeyCode::Numpad4 => Code::Numpad4,
dioxus_html::KeyCode::Numpad5 => Code::Numpad5,
dioxus_html::KeyCode::Numpad6 => Code::Numpad6,
dioxus_html::KeyCode::Numpad7 => Code::Numpad7,
dioxus_html::KeyCode::Numpad8 => Code::Numpad8,
dioxus_html::KeyCode::Numpad9 => Code::Numpad9,
dioxus_html::KeyCode::Multiply => Code::NumpadMultiply,
dioxus_html::KeyCode::Add => Code::NumpadAdd,
dioxus_html::KeyCode::Subtract => Code::NumpadSubtract,
dioxus_html::KeyCode::DecimalPoint => Code::NumpadDecimal,
dioxus_html::KeyCode::Divide => Code::NumpadDivide,
dioxus_html::KeyCode::F1 => Code::F1,
dioxus_html::KeyCode::F2 => Code::F2,
dioxus_html::KeyCode::F3 => Code::F3,
dioxus_html::KeyCode::F4 => Code::F4,
dioxus_html::KeyCode::F5 => Code::F5,
dioxus_html::KeyCode::F6 => Code::F6,
dioxus_html::KeyCode::F7 => Code::F7,
dioxus_html::KeyCode::F8 => Code::F8,
dioxus_html::KeyCode::F9 => Code::F9,
dioxus_html::KeyCode::F10 => Code::F10,
dioxus_html::KeyCode::F11 => Code::F11,
dioxus_html::KeyCode::F12 => Code::F12,
dioxus_html::KeyCode::NumLock => Code::NumLock,
dioxus_html::KeyCode::ScrollLock => Code::ScrollLock,
dioxus_html::KeyCode::Semicolon => Code::Semicolon,
dioxus_html::KeyCode::EqualSign => Code::Equal,
dioxus_html::KeyCode::Comma => Code::Comma,
dioxus_html::KeyCode::Period => Code::Period,
dioxus_html::KeyCode::ForwardSlash => Code::Slash,
dioxus_html::KeyCode::GraveAccent => Code::Backquote,
dioxus_html::KeyCode::OpenBracket => Code::BracketLeft,
dioxus_html::KeyCode::BackSlash => Code::Backslash,
dioxus_html::KeyCode::CloseBraket => Code::BracketRight,
dioxus_html::KeyCode::SingleQuote => Code::Quote,
key => panic!("Failed to convert {:?} to tao::keyboard::KeyCode, try using tao::keyboard::KeyCode directly", key),
}
}
+6 -8
View File
@@ -1,14 +1,15 @@
use crate::desktop_context::{EventData, UserWindowEvent};
use crate::ipc::{EventData, UserWindowEvent};
use futures_util::task::ArcWake;
use std::sync::Arc;
use wry::application::{event_loop::EventLoopProxy, window::WindowId};
use tao::{event_loop::EventLoopProxy, window::WindowId};
/// Create a waker that will send a poll event to the event loop.
///
/// This lets the VirtualDom "come up for air" and process events while the main thread is blocked by the WebView.
///
/// All other IO lives in the Tokio runtime,
pub fn tao_waker(proxy: &EventLoopProxy<UserWindowEvent>, id: WindowId) -> std::task::Waker {
/// All IO and multithreading lives on other threads. Thanks to tokio's work stealing approach, the main thread can never
/// claim a task while it's blocked by the event loop.
pub fn tao_waker(proxy: EventLoopProxy<UserWindowEvent>, id: WindowId) -> std::task::Waker {
struct DomHandle {
proxy: EventLoopProxy<UserWindowEvent>,
id: WindowId,
@@ -27,8 +28,5 @@ pub fn tao_waker(proxy: &EventLoopProxy<UserWindowEvent>, id: WindowId) -> std::
}
}
futures_util::task::waker(Arc::new(DomHandle {
id,
proxy: proxy.clone(),
}))
futures_util::task::waker(Arc::new(DomHandle { id, proxy }))
}
+174 -84
View File
@@ -1,96 +1,186 @@
use crate::desktop_context::EventData;
use crate::protocol;
use crate::{desktop_context::UserWindowEvent, Config};
use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
pub use wry;
pub use wry::application as tao;
use wry::application::window::Window;
use wry::webview::{WebContext, WebView, WebViewBuilder};
use crate::{
app::SharedContext,
assets::AssetHandlerRegistry,
edits::EditQueue,
eval::DesktopEvalProvider,
ipc::{EventData, UserWindowEvent},
protocol::{self},
waker::tao_waker,
Config, DesktopContext, DesktopService,
};
use dioxus_core::VirtualDom;
use dioxus_html::prelude::EvalProvider;
use futures_util::{pin_mut, FutureExt};
use std::{rc::Rc, task::Waker};
use wry::{RequestAsyncResponder, WebContext, WebViewBuilder};
pub fn build(
cfg: &mut Config,
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
proxy: EventLoopProxy<UserWindowEvent>,
) -> (WebView, WebContext) {
let builder = cfg.window.clone();
let window = builder.build(event_loop).unwrap();
let file_handler = cfg.file_drop_handler.take();
let custom_head = cfg.custom_head.clone();
let index_file = cfg.custom_index.clone();
let root_name = cfg.root_name.clone();
pub struct WebviewInstance {
pub dom: VirtualDom,
pub desktop_context: DesktopContext,
pub waker: Waker,
// We assume that if the icon is None in cfg, then the user just didnt set it
if cfg.window.window.window_icon.is_none() {
window.set_window_icon(Some(
tao::window::Icon::from_rgba(
include_bytes!("./assets/default_icon.bin").to_vec(),
460,
460,
)
.expect("image parse failed"),
));
}
// Wry assumes the webcontext is alive for the lifetime of the webview.
// We need to keep the webcontext alive, otherwise the webview will crash
_web_context: WebContext,
}
let mut web_context = WebContext::new(cfg.data_dir.clone());
impl WebviewInstance {
pub fn new(mut cfg: Config, dom: VirtualDom, shared: Rc<SharedContext>) -> WebviewInstance {
let window = cfg.window.clone().build(&shared.target).unwrap();
let mut webview = WebViewBuilder::new(window)
.unwrap()
.with_transparent(cfg.window.window.transparent)
.with_url("dioxus://index.html/")
.unwrap()
.with_ipc_handler(move |window: &Window, payload: String| {
// TODO: allow users to specify their own menubars, again :/
#[cfg(not(any(target_os = "android", target_os = "ios")))]
crate::menubar::build_menu(&window, cfg.enable_default_menu_bar);
// We assume that if the icon is None in cfg, then the user just didnt set it
if cfg.window.window.window_icon.is_none() {
window.set_window_icon(Some(
tao::window::Icon::from_rgba(
include_bytes!("./assets/default_icon.bin").to_vec(),
460,
460,
)
.expect("image parse failed"),
));
}
let mut web_context = WebContext::new(cfg.data_dir.clone());
let edit_queue = EditQueue::default();
let asset_handlers = AssetHandlerRegistry::new(dom.runtime());
let headless = !cfg.window.window.visible;
// Rust :(
let window_id = window.id();
let file_handler = cfg.file_drop_handler.take();
let custom_head = cfg.custom_head.clone();
let index_file = cfg.custom_index.clone();
let root_name = cfg.root_name.clone();
let asset_handlers_ = asset_handlers.clone();
let edit_queue_ = edit_queue.clone();
let proxy_ = shared.proxy.clone();
let request_handler = move |request, responder: RequestAsyncResponder| {
// Try to serve the index file first
let index_bytes = protocol::index_request(
&request,
custom_head.clone(),
index_file.clone(),
&root_name,
headless,
);
// Otherwise, try to serve an asset, either from the user or the filesystem
match index_bytes {
Some(body) => responder.respond(body),
None => protocol::desktop_handler(
request,
asset_handlers_.clone(),
&edit_queue_,
responder,
),
}
};
let ipc_handler = move |payload: String| {
// defer the event to the main thread
if let Ok(message) = serde_json::from_str(&payload) {
_ = proxy.send_event(UserWindowEvent(EventData::Ipc(message), window.id()));
_ = proxy_.send_event(UserWindowEvent(EventData::Ipc(message), window_id));
}
})
.with_custom_protocol(String::from("dioxus"), move |r| {
protocol::desktop_handler(r, custom_head.clone(), index_file.clone(), &root_name)
})
.with_web_context(&mut web_context);
};
if let Some(handler) = file_handler {
webview = webview.with_file_drop_handler(handler)
let mut webview = WebViewBuilder::new(&window)
.with_transparent(cfg.window.window.transparent)
.with_url("dioxus://index.html/")
.unwrap()
.with_ipc_handler(ipc_handler)
.with_asynchronous_custom_protocol(String::from("dioxus"), request_handler)
.with_file_drop_handler(file_drop_handler)
.with_web_context(&mut web_context);
if let Some(handler) = file_handler {
webview = webview.with_file_drop_handler(handler)
}
// This was removed from wry, I'm not sure what replaced it
// #[cfg(windows)]
// {
// // Windows has a platform specific settings to disable the browser shortcut keys
// use wry::WebViewBuilderExtWindows;
// webview = webview.with_browser_accelerator_keys(false);
// }
if let Some(color) = cfg.background_color {
webview = webview.with_background_color(color);
}
for (name, handler) in cfg.protocols.drain(..) {
webview = webview.with_custom_protocol(name, handler);
}
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 {
// in release mode, we don't want to show the dev tool or reload menus
webview = webview.with_initialization_script(INITIALIZATION_SCRIPT)
} else {
// in debug, we are okay with the reload menu showing and dev tool
webview = webview.with_devtools(true);
}
let desktop_context = Rc::from(DesktopService::new(
webview.build().unwrap(),
window,
shared.clone(),
edit_queue,
asset_handlers,
));
// Provide the desktop context to the virtualdom
dom.base_scope().provide_context(desktop_context.clone());
// Also set up its eval provider
// It's important that we provide as dyn EvalProvider - using the concrete type has
// a different TypeId.
let provider: Rc<dyn EvalProvider> =
Rc::new(DesktopEvalProvider::new(desktop_context.clone()));
dom.base_scope().provide_context(provider);
WebviewInstance {
waker: tao_waker(shared.proxy.clone(), desktop_context.window.id()),
desktop_context,
dom,
_web_context: web_context,
}
}
#[cfg(windows)]
{
// Windows has a platform specific settings to disable the browser shortcut keys
use wry::webview::WebViewBuilderExtWindows;
webview = webview.with_browser_accelerator_keys(false);
pub fn poll_vdom(&mut self) {
let mut cx = std::task::Context::from_waker(&self.waker);
// Continously poll the virtualdom until it's pending
// Wait for work will return Ready when it has edits to be sent to the webview
// It will return Pending when it needs to be polled again - nothing is ready
loop {
{
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,
}
}
self.desktop_context.send_edits(self.dom.render_immediate());
}
}
if let Some(color) = cfg.background_color {
webview = webview.with_background_color(color);
}
// These are commented out because wry is currently broken in wry
// let mut web_context = WebContext::new(cfg.data_dir.clone());
// .with_web_context(&mut web_context);
for (name, handler) in cfg.protocols.drain(..) {
webview = webview.with_custom_protocol(name, handler)
}
if cfg.disable_context_menu {
// in release mode, we don't want to show the dev tool or reload menus
webview = webview.with_initialization_script(
r#"
if (document.addEventListener) {
document.addEventListener('contextmenu', function(e) {
e.preventDefault();
}, false);
} else {
document.attachEvent('oncontextmenu', function() {
window.event.returnValue = false;
});
}
"#,
)
} else {
// in debug, we are okay with the reload menu showing and dev tool
webview = webview.with_devtools(true);
}
(webview.build().unwrap(), web_context)
}