From 9a6d74533967dd57bc7eda8a42626d2d616f5930 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 15:21:30 -0800 Subject: [PATCH] Refactor desktop into App type --- examples/mobile_demo/Cargo.toml | 2 +- packages/core/src/error_boundary.rs | 6 +- packages/desktop/Cargo.toml | 9 +- packages/desktop/src/app.rs | 569 ++++++++++++++++++++++++ packages/desktop/src/asset_handler.rs | 0 packages/desktop/src/cfg.rs | 9 +- packages/desktop/src/desktop_context.rs | 76 ++-- packages/desktop/src/events.rs | 22 +- packages/desktop/src/hooks.rs | 1 + packages/desktop/src/lib.rs | 550 ++--------------------- packages/desktop/src/protocol.rs | 10 +- packages/desktop/src/shortcut.rs | 2 +- packages/desktop/src/waker.rs | 4 +- packages/desktop/src/webview.rs | 111 ++--- packages/interpreter/Cargo.toml | 2 +- 15 files changed, 728 insertions(+), 645 deletions(-) create mode 100644 packages/desktop/src/app.rs create mode 100644 packages/desktop/src/asset_handler.rs create mode 100644 packages/desktop/src/hooks.rs diff --git a/examples/mobile_demo/Cargo.toml b/examples/mobile_demo/Cargo.toml index a90ff4f36..e980774e7 100644 --- a/examples/mobile_demo/Cargo.toml +++ b/examples/mobile_demo/Cargo.toml @@ -35,7 +35,7 @@ frameworks = ["WebKit"] [dependencies] anyhow = "1.0.56" log = "0.4.11" -wry = "0.34.0" +wry = "0.35.0" dioxus = { path = "../../packages/dioxus" } dioxus-desktop = { path = "../../packages/desktop", features = [ "tokio_runtime", diff --git a/packages/core/src/error_boundary.rs b/packages/core/src/error_boundary.rs index d42f59395..d4fe7c1ba 100644 --- a/packages/core/src/error_boundary.rs +++ b/packages/core/src/error_boundary.rs @@ -6,7 +6,7 @@ use crate::{ use std::{ any::{Any, TypeId}, backtrace::Backtrace, - cell::RefCell, + cell::{Cell, RefCell}, error::Error, fmt::{Debug, Display}, rc::Rc, @@ -472,8 +472,8 @@ pub fn ErrorBoundary<'a>(cx: Scope<'a, ErrorBoundaryProps<'a>>) -> Element { attr_paths: &[], }; VNode { - parent: Default::default(), - stable_id: Default::default(), + parent: Cell::new(None), + stable_id: Cell::new(None), key: None, template: std::cell::Cell::new(TEMPLATE), root_ids: bumpalo::collections::Vec::with_capacity_in(1usize, __cx.bump()).into(), diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 1064d2c1e..4eed74b84 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -19,7 +19,10 @@ serde = "1.0.136" serde_json = "1.0.79" thiserror = { workspace = true } tracing = { workspace = true } -wry = { version = "0.34.0", default-features = false, features = ["tao", "protocol", "file-drop"] } +wry = { version = "0.35.0", default-features = false, features = [ + "protocol", + "file-drop", +] } futures-channel = { workspace = true } tokio = { workspace = true, features = [ "sync", @@ -39,11 +42,13 @@ futures-util = { workspace = true } urlencoding = "2.1.2" async-trait = "0.1.68" crossbeam-channel = "0.5.8" +tao = { version = "0.24.0", features = ["rwh_05"] } +muda = "0.11.3" [target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies] rfd = "0.12" -global-hotkey = { git = "https://github.com/tauri-apps/global-hotkey" } +global-hotkey = "0.4.1" [target.'cfg(target_os = "ios")'.dependencies] objc = "0.2.7" diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs new file mode 100644 index 000000000..f88970f03 --- /dev/null +++ b/packages/desktop/src/app.rs @@ -0,0 +1,569 @@ +pub use crate::cfg::{Config, WindowCloseBehaviour}; +pub use crate::desktop_context::DesktopContext; +pub use crate::desktop_context::{ + use_window, use_wry_event_handler, window, DesktopService, WryEventHandler, WryEventHandlerId, +}; +use crate::desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers}; +use crate::events::{IpcMessage, KnownIpcMethod}; +use crate::file_upload; +use crate::query::QueryResult; +use crate::shortcut::GlobalHotKeyEvent; +use dioxus_core::*; +use dioxus_html::{event_bubbles, MountedData}; +use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent}; +use global_hotkey::{ + hotkey::{Code, HotKey, Modifiers}, + GlobalHotKeyManager, +}; +// use dioxus_interpreter_js::binary_protocol::Channel; +use crate::element::DesktopElement; +use crate::eval::init_eval; +pub use crate::protocol::{ + use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse, +}; +use crate::shortcut::ShortcutRegistry; +pub use crate::shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError}; +use futures_util::{pin_mut, FutureExt}; +use rustc_hash::FxHashMap; +use std::rc::Rc; +use std::sync::atomic::AtomicU16; +use std::task::Waker; +use std::{borrow::Borrow, cell::Cell}; +use std::{collections::HashMap, sync::Arc}; +pub use tao::dpi::{LogicalSize, PhysicalSize}; +use tao::event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}; +pub use tao::window::WindowBuilder; +use tao::window::WindowId; +use tao::{ + event::{Event, StartCause, WindowEvent}, + event_loop::ControlFlow, +}; +use tao::{event_loop::EventLoopBuilder, window::Window}; +use tokio::runtime::Builder; +pub use wry; +use wry::WebContext; +use wry::WebView; + +pub struct App

{ + // 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 + pub(crate) props: Rc>>, + pub(crate) cfg: Rc>>, + pub(crate) root: Component

, + pub(crate) webviews: HashMap, + pub(crate) event_handlers: WindowEventHandlers, + pub(crate) queue: WebviewQueue, + pub(crate) shortcut_manager: ShortcutRegistry, + pub(crate) global_hotkey_channel: crossbeam_channel::Receiver, + pub(crate) proxy: EventLoopProxy, + pub(crate) window_behavior: WindowCloseBehaviour, + pub(crate) control_flow: ControlFlow, + pub(crate) is_visible_before_start: bool, +} + +impl

App

{ + pub fn new(cfg: Config, props: P, root: Component

) -> (EventLoop, Self) { + let event_loop = EventLoopBuilder::::with_user_event().build(); + + let mut app = Self { + root, + window_behavior: cfg.last_window_close_behaviour, + is_visible_before_start: true, + webviews: HashMap::new(), + event_handlers: WindowEventHandlers::default(), + queue: WebviewQueue::default(), + shortcut_manager: ShortcutRegistry::new(), + global_hotkey_channel: GlobalHotKeyEvent::receiver().clone(), + proxy: event_loop.create_proxy(), + props: Rc::new(Cell::new(Some(props))), + cfg: Rc::new(Cell::new(Some(cfg))), + control_flow: ControlFlow::Wait, + }; + + #[cfg(all(feature = "hot-reload", debug_assertions))] + app.connect_hotreload(); + + (event_loop, app) + } + + pub fn tick( + &mut self, + window_event: &Event<'_, UserWindowEvent>, + event_loop: &EventLoopWindowTarget, + ) { + // Set the control flow here, but make sure to update it at the end of the match + self.control_flow = ControlFlow::Wait; + + self.event_handlers.apply_event(window_event, event_loop); + + if let Ok(event) = self.global_hotkey_channel.try_recv() { + self.shortcut_manager.call_handlers(event); + } + } + + pub fn connect_hotreload(&mut self) { + let proxy = self.proxy.clone(); + dioxus_hot_reload::connect({ + move |template| { + let _ = proxy.send_event(UserWindowEvent( + EventData::HotReloadEvent(template), + unsafe { WindowId::dummy() }, + )); + } + }); + } + + // + pub fn handle_new_window(&mut self) { + for handler in self.queue.borrow_mut().drain(..) { + let id = handler.desktop_context.window.id(); + self.webviews.insert(id, handler); + _ = self.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, target: &EventLoopWindowTarget) { + let props = self.props.take().unwrap(); + let cfg = self.cfg.take().unwrap(); + + let dom = VirtualDom::new_with_props(self.root, props); + + self.is_visible_before_start = cfg.window.window.visible; + + let handler = create_new_window( + cfg, + target, + &self.proxy, + dom, + &self.queue, + &self.event_handlers, + self.shortcut_manager.clone(), + ); + + let id = handler.desktop_context.window.id(); + self.webviews.insert(id, handler); + + _ = self.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(); + send_edits(view.dom.rebuild(), &view.desktop_context); + 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_poll_msg(&mut self, id: WindowId) { + self.poll_vdom(id); + } + + pub fn handle_query_msg(&mut self, msg: IpcMessage, id: WindowId) { + let Ok(result) = serde_json::from_value::(msg.params()) else { + return; + }; + + let Some(view) = self.webviews.get(&id) else { + return; + }; + + view.dom + .base_scope() + .consume_context::() + .unwrap() + .query + .send(result); + } + + pub fn handle_user_event_msg(&mut self, msg: IpcMessage, id: WindowId) { + let params = msg.params(); + + let evt = match serde_json::from_value::(params) { + Ok(value) => value, + Err(err) => { + tracing::error!("Error parsing user_event: {:?}", err); + return; + } + }; + + let HtmlEvent { + element, + name, + bubbles, + data, + } = evt; + + let view = self.webviews.get_mut(&id).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::() + .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); + } + + #[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); + + // poll_vdom(webview); + todo!() + } + } + dioxus_hot_reload::HotReloadMsg::Shutdown => { + self.control_flow = ControlFlow::Exit; + } + } + } + + pub fn handle_file_dialog_msg(&self, msg: IpcMessage, window: WindowId) { + if let Ok(file_diolog) = + serde_json::from_value::(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 = 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); + } + + todo!() + // send_edits(view.dom.render_immediate(), &view.desktop_context); + } + } + + /// 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 view = self.webviews.get_mut(&id).unwrap(); + + 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); + } + } +} + +pub fn create_new_window( + mut cfg: Config, + event_loop: &EventLoopWindowTarget, + proxy: &EventLoopProxy, + dom: VirtualDom, + queue: &WebviewQueue, + event_handlers: &WindowEventHandlers, + shortcut_manager: ShortcutRegistry, +) -> WebviewHandler { + let (webview, web_context, asset_handlers, edit_queue, window) = + crate::webview::build(&mut cfg, event_loop, proxy.clone()); + + let desktop_context = Rc::from(DesktopService::new( + window, + webview, + proxy.clone(), + event_loop.clone(), + queue.clone(), + event_handlers.clone(), + shortcut_manager, + edit_queue, + asset_handlers, + )); + + 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: crate::waker::tao_waker(proxy, desktop_context.window.id()), + desktop_context, + dom, + _web_context: web_context, + } +} + +pub 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, +} + +/// Send a list of mutations to the webview +pub fn send_edits(edits: Mutations, desktop_context: &DesktopContext) { + let mut channel = desktop_context.channel.borrow_mut(); + let mut templates = desktop_context.templates.borrow_mut(); + if let Some(bytes) = apply_edits( + edits, + &mut channel, + &mut templates, + &desktop_context.max_template_count, + ) { + desktop_context.edit_queue.add_edits(bytes) + } +} + +pub struct Channel {} + +pub fn apply_edits( + mutations: Mutations, + channel: &mut Channel, + templates: &mut FxHashMap, + max_template_count: &AtomicU16, +) -> Option> { + use dioxus_core::Mutation::*; + if mutations.templates.is_empty() && mutations.edits.is_empty() { + return None; + } + for template in mutations.templates { + add_template(&template, channel, templates, max_template_count); + } + 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, + max_template_count: &AtomicU16, +) { + let current_max_template_count = max_template_count.load(std::sync::atomic::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, std::sync::atomic::Ordering::Relaxed); +} + +pub fn create_template_node(channel: &mut Channel, v: &'static TemplateNode<'static>) { + use TemplateNode::*; + match v { + 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(), + } +} + +/// Different hide implementations per platform +#[allow(unused)] +pub fn hide_app_window(webview: &WebView) { + #[cfg(target_os = "windows")] + { + use wry::application::platform::windows::WindowExtWindows; + window.set_visible(false); + window.set_skip_taskbar(true); + } + + #[cfg(target_os = "linux")] + { + use wry::application::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::(); + let _: () = msg_send![app, hide: nil]; + }); + } +} diff --git a/packages/desktop/src/asset_handler.rs b/packages/desktop/src/asset_handler.rs new file mode 100644 index 000000000..e69de29bb diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 596b21938..9d1854bdf 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -1,11 +1,10 @@ use std::borrow::Cow; use std::path::PathBuf; -use wry::application::window::Icon; +use tao::window::{Icon, Window, WindowBuilder}; use wry::{ - application::window::{Window, WindowBuilder}, http::{Request as HttpRequest, Response as HttpResponse}, - webview::FileDropEvent, + FileDropEvent, }; // pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView); @@ -38,7 +37,7 @@ pub struct Config { pub(crate) enable_default_menu_bar: bool, } -type DropHandler = Box bool>; +type DropHandler = Box bool>; pub(crate) type WryProtocol = ( String, @@ -120,7 +119,7 @@ impl Config { /// Set a file drop handler pub fn with_file_drop_handler( mut self, - handler: impl Fn(&Window, FileDropEvent) -> bool + 'static, + handler: impl Fn(FileDropEvent) -> bool + 'static, ) -> Self { self.file_drop_handler = Some(Box::new(handler)); self diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 62a574d9b..763c00c39 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,38 +1,29 @@ -use crate::create_new_window; -use crate::events::IpcMessage; use crate::protocol::AssetFuture; use crate::protocol::AssetHandlerRegistry; use crate::query::QueryEngine; use crate::shortcut::{HotKey, ShortcutId, ShortcutRegistry, ShortcutRegistryError}; use crate::AssetHandler; use crate::Config; -use crate::WebviewHandler; +use crate::{app::WebviewHandler, events::IpcMessage}; use dioxus_core::ScopeState; use dioxus_core::VirtualDom; -#[cfg(all(feature = "hot-reload", debug_assertions))] -use dioxus_hot_reload::HotReloadMsg; use dioxus_interpreter_js::binary_protocol::Channel; use rustc_hash::FxHashMap; use slab::Slab; -use std::cell::RefCell; -use std::fmt::Debug; -use std::fmt::Formatter; -use std::rc::Rc; -use std::rc::Weak; -use std::sync::atomic::AtomicU16; -use std::sync::Arc; -use std::sync::Mutex; -use wry::application::event::Event; -use wry::application::event_loop::EventLoopProxy; -use wry::application::event_loop::EventLoopWindowTarget; -#[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 std::{ + cell::RefCell, fmt::Debug, fmt::Formatter, rc::Rc, rc::Weak, sync::atomic::AtomicU16, + sync::Arc, sync::Mutex, +}; +use tao::{ + event::Event, + event_loop::{EventLoopProxy, EventLoopWindowTarget}, + window::{Fullscreen as WryFullscreen, Window, WindowId}, +}; -pub type ProxyType = EventLoopProxy; +use wry::{RequestAsyncResponder, WebView}; + +#[cfg(target_os = "ios")] +use tao::platform::ios::WindowExtIOS; /// Get an imperative handle to the current window without using a hook /// @@ -44,7 +35,6 @@ pub fn window() -> DesktopContext { } /// Get an imperative handle to the current window -#[deprecated = "Prefer the using the `window` function directly for cleaner code"] pub fn use_window(cx: &ScopeState) -> &DesktopContext { cx.use_hook(|| cx.consume_context::()) .as_ref() @@ -56,7 +46,7 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext { #[derive(Default, Clone)] pub(crate) struct EditQueue { queue: Arc>>>, - responder: Arc>>, + responder: Arc>>, } impl Debug for EditQueue { @@ -71,7 +61,7 @@ impl Debug for EditQueue { } impl EditQueue { - pub fn handle_request(&self, responder: wry::webview::RequestAsyncResponder) { + 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)); @@ -106,10 +96,12 @@ pub(crate) type WebviewQueue = Rc>>; /// ``` pub struct DesktopService { /// The wry/tao proxy to the current window - pub webview: Rc, + pub webview: WebView, + + pub window: Window, /// The proxy to the event loop - pub proxy: ProxyType, + pub proxy: EventLoopProxy, /// The receiver for queries about the current window pub(super) query: QueryEngine, @@ -141,14 +133,15 @@ 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( + window: Window, webview: WebView, - proxy: ProxyType, + proxy: EventLoopProxy, event_loop: EventLoopWindowTarget, webviews: WebviewQueue, event_handlers: WindowEventHandlers, @@ -157,7 +150,8 @@ impl DesktopService { asset_handlers: AssetHandlerRegistry, ) -> Self { Self { - webview: Rc::new(webview), + window, + webview, proxy, event_loop, query: Default::default(), @@ -198,7 +192,7 @@ impl DesktopService { .consume_context::>() .unwrap(); - let id = window.desktop_context.webview.window().id(); + let id = window.desktop_context.window.id(); self.proxy .send_event(UserWindowEvent(EventData::NewWindow, id)) @@ -222,7 +216,7 @@ impl DesktopService { /// onmousedown: move |_| { desktop.drag_window(); } /// ``` pub fn drag(&self) { - let window = self.webview.window(); + let window = &self.window; // if the drag_window has any errors, we don't do anything if window.fullscreen().is_none() { @@ -232,7 +226,7 @@ impl DesktopService { /// Toggle whether the window is maximized or not pub fn toggle_maximized(&self) { - let window = self.webview.window(); + let window = &self.window; window.set_maximized(!window.is_maximized()) } @@ -253,10 +247,10 @@ impl DesktopService { /// 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()))), + ); } } @@ -336,7 +330,7 @@ impl DesktopService { /// Push an objc view to the window #[cfg(target_os = "ios")] pub fn push_view(&self, view: objc_id::ShareId) { - let window = self.webview.window(); + let window = &self.window; unsafe { use objc::runtime::Object; @@ -356,7 +350,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; @@ -380,7 +374,7 @@ pub enum EventData { Ipc(IpcMessage), #[cfg(all(feature = "hot-reload", debug_assertions))] - HotReloadEvent(HotReloadMsg), + HotReloadEvent(dioxus_hot_reload::HotReloadMsg), NewWindow, diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index 7059a7c85..a877b52d6 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -8,9 +8,27 @@ pub struct IpcMessage { params: serde_json::Value, } +#[derive(Deserialize, Serialize, Debug, Clone)] +pub enum KnownIpcMethod<'a> { + FileDialog, + UserEvent, + Query, + BrowserOpen, + Initialize, + Other(&'a str), +} + impl IpcMessage { - pub(crate) fn method(&self) -> &str { - self.method.as_str() + pub(crate) fn method(&self) -> KnownIpcMethod { + match self.method.as_str() { + // todo: this is a misspelling + "file_diolog" => KnownIpcMethod::FileDialog, + "user_event" => KnownIpcMethod::UserEvent, + "query" => KnownIpcMethod::Query, + "browser_open" => KnownIpcMethod::BrowserOpen, + "initialize" => KnownIpcMethod::Initialize, + _ => KnownIpcMethod::Other(&self.method), + } } pub(crate) fn params(self) -> serde_json::Value { diff --git a/packages/desktop/src/hooks.rs b/packages/desktop/src/hooks.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/packages/desktop/src/hooks.rs @@ -0,0 +1 @@ + diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 9460ca87d..1fc3e99c0 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -3,6 +3,8 @@ #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] #![deny(missing_docs)] +mod app; +mod asset_handler; mod cfg; mod desktop_context; mod element; @@ -10,6 +12,7 @@ mod escape; mod eval; mod events; mod file_upload; +mod hooks; #[cfg(any(target_os = "ios", target_os = "android"))] mod mobile_shortcut; mod protocol; @@ -18,44 +21,24 @@ mod shortcut; mod waker; mod webview; -use crate::query::QueryResult; -use crate::shortcut::GlobalHotKeyEvent; pub use cfg::{Config, WindowCloseBehaviour}; +pub use desktop_context::use_window; pub use desktop_context::DesktopContext; #[allow(deprecated)] pub use desktop_context::{ - use_window, use_wry_event_handler, window, DesktopService, WryEventHandler, WryEventHandlerId, + use_wry_event_handler, window, DesktopService, WryEventHandler, WryEventHandlerId, }; -use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers}; +use desktop_context::{EventData, UserWindowEvent}; use dioxus_core::*; -use dioxus_html::{event_bubbles, MountedData}; -use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent}; -use dioxus_interpreter_js::binary_protocol::Channel; -use element::DesktopElement; -use eval::init_eval; -use futures_util::{pin_mut, FutureExt}; +use events::KnownIpcMethod; pub use protocol::{use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse}; -use rustc_hash::FxHashMap; -use shortcut::ShortcutRegistry; pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError}; -use std::cell::Cell; -use std::rc::Rc; -use std::sync::atomic::AtomicU16; -use std::task::Waker; -use std::{collections::HashMap, sync::Arc}; pub use tao::dpi::{LogicalSize, PhysicalSize}; -use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; +use tao::event::{Event, StartCause, WindowEvent}; pub use tao::window::WindowBuilder; -use tao::{ - event::{Event, StartCause, WindowEvent}, - event_loop::ControlFlow, -}; -// pub use webview::build_default_menu_bar; +use tokio::runtime::Builder; +pub use webview::build_default_menu_bar; pub use wry; -pub use wry::application as tao; -use wry::application::event_loop::EventLoopBuilder; -use wry::webview::WebView; -use wry::{application::window::WindowId, webview::WebContext}; /// Launch the WebView and run the event loop. /// @@ -127,512 +110,47 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// } /// ``` pub fn launch_with_props(root: Component

, props: P, cfg: Config) { - let event_loop = EventLoopBuilder::::with_user_event().build(); - - 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() + // I would love to just allow dioxus to work with any runtime... but tokio is weird + let _guard = Builder::new_multi_thread() .enable_all() .build() - .unwrap(); + .unwrap() + .enter(); - // 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::::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(); - let global_hotkey_channel = GlobalHotKeyEvent::receiver(); - - // move the props into a cell so we can pop it out later to create the first window - // iOS panics if we create a window before the event loop is started - let props = Rc::new(Cell::new(Some(props))); - let cfg = Rc::new(Cell::new(Some(cfg))); - let mut is_visible_before_start = true; + let (event_loop, mut app) = app::App::new(cfg, props, root); event_loop.run(move |window_event, event_loop, control_flow| { - *control_flow = ControlFlow::Poll; - - event_handlers.apply_event(&window_event, event_loop); - - if let Ok(event) = global_hotkey_channel.try_recv() { - shortcut_manager.call_handlers(event); - } + app.tick(&window_event, event_loop); match window_event { + Event::NewEvents(StartCause::Init) => app.handle_start_cause_init(event_loop), 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 - } - } + WindowEvent::CloseRequested => app.handle_close_requested(window_id), + WindowEvent::Destroyed { .. } => app.window_destroyed(window_id), _ => {} }, - - 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); - - is_visible_before_start = cfg.window.window.visible; - - 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; - } + Event::UserEvent(UserWindowEvent(event, id)) => match event { + EventData::Poll => app.handle_poll_msg(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() { + KnownIpcMethod::FileDialog => app.handle_file_dialog_msg(msg, id), + KnownIpcMethod::UserEvent => app.handle_user_event_msg(msg, id), + KnownIpcMethod::Query => app.handle_query_msg(msg, id), + KnownIpcMethod::BrowserOpen => app.handle_browser_open(msg), + KnownIpcMethod::Initialize => app.handle_initialize_msg(id), + KnownIpcMethod::Other(_) => {} }, - - 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::(params) { - Ok(value) => value, - Err(err) => { - tracing::error!("Error parsing user_event: {:?}", 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::() - .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); - } - - // 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::(params) { - let view = webviews.get(&event.1).unwrap(); - let query = view - .dom - .base_scope() - .consume_context::() - .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); - view.desktop_context - .webview - .window() - .set_visible(is_visible_before_start); - } - - 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::(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); - } - } - - _ => {} }, _ => {} } + + // Update the control flow + *control_flow = app.control_flow; }) } - -fn create_new_window( - mut cfg: Config, - event_loop: &EventLoopWindowTarget, - proxy: &EventLoopProxy, - dom: VirtualDom, - queue: &WebviewQueue, - event_handlers: &WindowEventHandlers, - shortcut_manager: ShortcutRegistry, -) -> WebviewHandler { - let (webview, web_context, asset_handlers, edit_queue) = - 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, - edit_queue, - asset_handlers, - )); - - 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); - } -} - -/// Send a list of mutations to the webview -fn send_edits(edits: Mutations, desktop_context: &DesktopContext) { - let mut channel = desktop_context.channel.borrow_mut(); - let mut templates = desktop_context.templates.borrow_mut(); - if let Some(bytes) = apply_edits( - edits, - &mut channel, - &mut templates, - &desktop_context.max_template_count, - ) { - desktop_context.edit_queue.add_edits(bytes) - } -} - -fn apply_edits( - mutations: Mutations, - channel: &mut Channel, - templates: &mut FxHashMap, - max_template_count: &AtomicU16, -) -> Option> { - use dioxus_core::Mutation::*; - if mutations.templates.is_empty() && mutations.edits.is_empty() { - return None; - } - for template in mutations.templates { - add_template(&template, channel, templates, max_template_count); - } - 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) -} - -fn add_template( - template: &Template<'static>, - channel: &mut Channel, - templates: &mut FxHashMap, - max_template_count: &AtomicU16, -) { - let current_max_template_count = max_template_count.load(std::sync::atomic::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, std::sync::atomic::Ordering::Relaxed); -} - -fn create_template_node(channel: &mut Channel, v: &'static TemplateNode<'static>) { - use TemplateNode::*; - match v { - 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(), - } -} - -/// 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::(); - let _: () = msg_send![app, hide: nil]; - }); - } -} diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 61c6af661..36400ce95 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -1,4 +1,4 @@ -use crate::{window, DesktopContext}; +use crate::{use_window, DesktopContext}; use dioxus_core::ScopeState; use dioxus_interpreter_js::INTERPRETER_JS; use slab::Slab; @@ -17,7 +17,7 @@ use tokio::{ }; use wry::{ http::{status::StatusCode, Request, Response}, - Result, + RequestAsyncResponder, Result, }; use crate::desktop_context::EditQueue; @@ -259,8 +259,7 @@ pub(super) async fn desktop_handler( .body(Cow::from(body)) { Ok(response) => { - responder.respond(response); - return; + return Ok(response); } Err(err) => tracing::error!("error building response: {}", err), } @@ -307,8 +306,7 @@ pub(super) async fn desktop_handler( .body(Cow::from(asset)) { Ok(response) => { - responder.respond(response); - return; + return response; } Err(err) => tracing::error!("error building response: {}", err), } diff --git a/packages/desktop/src/shortcut.rs b/packages/desktop/src/shortcut.rs index bf8ed1382..55d49b405 100644 --- a/packages/desktop/src/shortcut.rs +++ b/packages/desktop/src/shortcut.rs @@ -3,7 +3,7 @@ use std::{cell::RefCell, collections::HashMap, rc::Rc, str::FromStr}; use dioxus_core::ScopeState; use dioxus_html::input_data::keyboard_types::Modifiers; use slab::Slab; -use wry::application::keyboard::ModifiersState; +use tao::keyboard::ModifiersState; use crate::{desktop_context::DesktopContext, window}; diff --git a/packages/desktop/src/waker.rs b/packages/desktop/src/waker.rs index 1e34baffc..11f23d8d8 100644 --- a/packages/desktop/src/waker.rs +++ b/packages/desktop/src/waker.rs @@ -1,7 +1,7 @@ use crate::desktop_context::{EventData, UserWindowEvent}; -use futures_util::task::ArcWake; +use futures_util::{pin_mut, task::ArcWake, FutureExt}; 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. /// diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index de5ff121c..d98e60099 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -1,20 +1,18 @@ use crate::desktop_context::{EditQueue, EventData}; use crate::protocol::{self, AssetHandlerRegistry}; use crate::{desktop_context::UserWindowEvent, Config}; +use muda::{Menu, MenuItem, PredefinedMenuItem, Submenu}; use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; -pub use wry; -pub use wry::application as tao; -use wry::application::window::Window; +use tao::window::Window; use wry::http::Response; -use wry::webview::{WebContext, WebView, WebViewBuilder}; +use wry::{WebContext, WebView, WebViewBuilder}; pub(crate) fn build( cfg: &mut Config, event_loop: &EventLoopWindowTarget, proxy: EventLoopProxy, -) -> (WebView, WebContext, AssetHandlerRegistry, EditQueue) { +) -> (WebView, WebContext, AssetHandlerRegistry, EditQueue, Window) { let builder = cfg.window.clone(); - let window = builder.with_visible(false).build(event_loop).unwrap(); let file_handler = cfg.file_drop_handler.take(); let custom_head = cfg.custom_head.clone(); let index_file = cfg.custom_index.clone(); @@ -43,15 +41,14 @@ pub(crate) fn build( let asset_handlers = AssetHandlerRegistry::new(); let asset_handlers_ref = asset_handlers.clone(); - let mut webview = WebViewBuilder::new(window) - .unwrap() + let mut webview = WebViewBuilder::new(&window) .with_transparent(cfg.window.window.transparent) .with_url("dioxus://index.html/") .unwrap() - .with_ipc_handler(move |window: &Window, payload: String| { + .with_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_asynchronous_custom_protocol(String::from("dioxus"), { @@ -77,10 +74,10 @@ pub(crate) fn build( }); } }) - .with_file_drop_handler(move |window, evet| { + .with_file_drop_handler(move |event| { file_handler .as_ref() - .map(|handler| handler(window, evet)) + .map(|handler| handler(event)) .unwrap_or_default() }) .with_web_context(&mut web_context); @@ -129,63 +126,47 @@ pub(crate) fn build( web_context, asset_handlers, edit_queue, + window, ) } -// /// 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() -> MenuBar { -// let mut menu_bar = MenuBar::new(); +/// 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 mut window_menu = MenuBar::new(); -// #[cfg(target_os = "macos")] -// { -// window_menu.add_native_item(MenuItem::EnterFullScreen); -// window_menu.add_native_item(MenuItem::Zoom); -// window_menu.add_native_item(MenuItem::Separator); -// } + // 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), + ]); -// window_menu.add_native_item(MenuItem::Hide); + 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), + ]); -// #[cfg(target_os = "macos")] -// { -// window_menu.add_native_item(MenuItem::HideOthers); -// window_menu.add_native_item(MenuItem::ShowAll); -// } + menu.append_items(&[&window_menu, &edit_menu]); -// window_menu.add_native_item(MenuItem::Minimize); -// window_menu.add_native_item(MenuItem::CloseWindow); -// window_menu.add_native_item(MenuItem::Separator); -// window_menu.add_native_item(MenuItem::Quit); -// menu_bar.add_submenu("Window", true, window_menu); - -// // since tao supports none of the below items on linux we should only add them on macos/windows -// #[cfg(not(target_os = "linux"))] -// { -// let mut edit_menu = MenuBar::new(); -// #[cfg(target_os = "macos")] -// { -// edit_menu.add_native_item(MenuItem::Undo); -// edit_menu.add_native_item(MenuItem::Redo); -// edit_menu.add_native_item(MenuItem::Separator); -// } - -// edit_menu.add_native_item(MenuItem::Cut); -// edit_menu.add_native_item(MenuItem::Copy); -// edit_menu.add_native_item(MenuItem::Paste); - -// #[cfg(target_os = "macos")] -// { -// edit_menu.add_native_item(MenuItem::Separator); -// edit_menu.add_native_item(MenuItem::SelectAll); -// } -// menu_bar.add_submenu("Edit", true, edit_menu); -// } - -// menu_bar -// } + menu +} diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index 7d046fbc8..153b69ba6 100644 --- a/packages/interpreter/Cargo.toml +++ b/packages/interpreter/Cargo.toml @@ -14,7 +14,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] wasm-bindgen = { workspace = true, optional = true } js-sys = { version = "0.3.56", optional = true } web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] } -sledgehammer_bindgen = { git = "https://github.com/ealmloff/sledgehammer_bindgen", default-features = false, optional = true } +sledgehammer_bindgen = { version = "0.3.0", default-features = false, optional = true } sledgehammer_utils = { version = "0.2", optional = true } serde = { version = "1.0", features = ["derive"], optional = true }