From 9a6d74533967dd57bc7eda8a42626d2d616f5930 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 15:21:30 -0800 Subject: [PATCH 01/43] 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 } From 436635386d77ea7cb51965c9888263d602b2a404 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 16:28:15 -0800 Subject: [PATCH 02/43] Get compiling --- packages/desktop/src/app.rs | 37 +++++++++++++------------ packages/desktop/src/desktop_context.rs | 3 +- packages/desktop/src/protocol.rs | 10 +++---- packages/desktop/src/webview.rs | 16 +++++++---- 4 files changed, 35 insertions(+), 31 deletions(-) diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index f88970f03..15abc0d24 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -4,26 +4,26 @@ 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::element::DesktopElement; +use crate::eval::init_eval; use crate::events::{IpcMessage, KnownIpcMethod}; use crate::file_upload; +pub use crate::protocol::{ + use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse, +}; use crate::query::QueryResult; use crate::shortcut::GlobalHotKeyEvent; +use crate::shortcut::ShortcutRegistry; +pub use crate::shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError}; 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 futures_util::{pin_mut, FutureExt}; 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; @@ -61,7 +61,7 @@ pub struct App

{ pub(crate) is_visible_before_start: bool, } -impl

App

{ +impl App

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

) -> (EventLoop, Self) { let event_loop = EventLoopBuilder::::with_user_event().build(); @@ -279,9 +279,12 @@ impl

App

{ dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => { for webview in self.webviews.values_mut() { webview.dom.replace_template(template); + } - // poll_vdom(webview); - todo!() + let ids = self.webviews.keys().copied().collect::>(); + + for id in ids { + self.poll_vdom(id); } } dioxus_hot_reload::HotReloadMsg::Shutdown => { @@ -290,7 +293,7 @@ impl

App

{ } } - pub fn handle_file_dialog_msg(&self, msg: IpcMessage, window: WindowId) { + pub fn handle_file_dialog_msg(&mut self, msg: IpcMessage, window: WindowId) { if let Ok(file_diolog) = serde_json::from_value::(msg.params()) { @@ -385,9 +388,9 @@ pub fn create_new_window( } pub struct WebviewHandler { - dom: VirtualDom, - desktop_context: DesktopContext, - waker: Waker, + pub dom: VirtualDom, + pub desktop_context: DesktopContext, + pub 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 @@ -408,8 +411,6 @@ pub fn send_edits(edits: Mutations, desktop_context: &DesktopContext) { } } -pub struct Channel {} - pub fn apply_edits( mutations: Mutations, channel: &mut Channel, diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 763c00c39..0ed40d707 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -98,6 +98,7 @@ pub struct DesktopService { /// The wry/tao proxy to the current window pub webview: WebView, + /// The tao window itself pub window: Window, /// The proxy to the event loop @@ -176,7 +177,7 @@ 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 { - let window = create_new_window( + let window = crate::app::create_new_window( cfg, &self.event_loop, &self.proxy, diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 36400ce95..6e28ca67c 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -197,7 +197,7 @@ pub fn use_asset_handler( handler: impl AssetHandler, ) -> &AssetHandlerHandle { cx.use_hook(|| { - let desktop = window(); + let desktop = crate::window(); let handler_id = Rc::new(OnceCell::new()); let handler_id_ref = Rc::clone(&handler_id); let desktop_ref = Rc::clone(&desktop); @@ -220,7 +220,7 @@ pub(super) async fn desktop_handler( asset_handlers: &AssetHandlerRegistry, edit_queue: &EditQueue, headless: bool, - responder: wry::webview::RequestAsyncResponder, + responder: wry::RequestAsyncResponder, ) { let request = AssetRequest::from(request); @@ -259,7 +259,7 @@ pub(super) async fn desktop_handler( .body(Cow::from(body)) { Ok(response) => { - return Ok(response); + return responder.respond(response); } Err(err) => tracing::error!("error building response: {}", err), } @@ -305,9 +305,7 @@ pub(super) async fn desktop_handler( .header("Content-Type", content_type) .body(Cow::from(asset)) { - Ok(response) => { - return response; - } + Ok(response) => return responder.respond(response), Err(err) => tracing::error!("error building response: {}", err), } } diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index d98e60099..229fc3b9b 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -12,17 +12,21 @@ pub(crate) fn build( event_loop: &EventLoopWindowTarget, proxy: EventLoopProxy, ) -> (WebView, WebContext, AssetHandlerRegistry, EditQueue, Window) { - let builder = cfg.window.clone(); + let mut builder = cfg.window.clone(); + + // TODO: restore the menu bar with muda: https://github.com/tauri-apps/muda/blob/dev/examples/wry.rs + if cfg.enable_default_menu_bar { + // builder = builder.with_menu(build_default_menu_bar()); + } + + let window = builder.build(event_loop).unwrap(); + + 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(); - // TODO: restore the menu bar with muda: https://github.com/tauri-apps/muda/blob/dev/examples/wry.rs - // if cfg.enable_default_menu_bar { - // builder = builder.with_menu(build_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( From d38ce349b139aea30b6910a4d7d195ccf7bc9ed1 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 16:36:19 -0800 Subject: [PATCH 03/43] Tiny bit of cleanups --- packages/desktop/src/app.rs | 12 ++++---- packages/desktop/src/webview.rs | 54 ++++++++++++++++----------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index 15abc0d24..265991bea 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -91,16 +91,17 @@ impl App

{ 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); - } + _ = self + .global_hotkey_channel + .try_recv() + .map(|event| self.shortcut_manager.call_handlers(event)); } + #[cfg(all(feature = "hot-reload", debug_assertions))] pub fn connect_hotreload(&mut self) { let proxy = self.proxy.clone(); dioxus_hot_reload::connect({ @@ -317,8 +318,7 @@ impl App

{ view.dom.handle_event(event_name, data, id, event_bubbles); } - todo!() - // send_edits(view.dom.render_immediate(), &view.desktop_context); + send_edits(view.dom.render_immediate(), &view.desktop_context); } } diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 229fc3b9b..e1ec06269 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -97,10 +97,6 @@ pub(crate) fn build( 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, move |r| handler(r)) } @@ -145,32 +141,36 @@ pub fn build_default_menu_bar() -> Menu { // 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 + .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), - ]); + 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]); + menu.append_items(&[&window_menu, &edit_menu]).unwrap(); menu } From 1518c223f636c674740bb0a2b6237856c7608576 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 16:38:49 -0800 Subject: [PATCH 04/43] Don't drop tokio runtime immediately --- packages/desktop/src/lib.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 1fc3e99c0..96c763045 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -113,11 +113,8 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) // We start the tokio runtime *on this thread* // Any future we poll later will use this runtime to spawn tasks and for IO // 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() - .enter(); + let rt = &Builder::new_multi_thread().enable_all().build().unwrap(); + let _guard = rt.enter(); let (event_loop, mut app) = app::App::new(cfg, props, root); From 62a9583fffb32e274ee3075430adfeae3b1148e6 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 16:44:16 -0800 Subject: [PATCH 05/43] Fix merge conflict --- packages/desktop/headless_tests/events.rs | 6 ------ packages/desktop/src/lib.rs | 1 - 2 files changed, 7 deletions(-) diff --git a/packages/desktop/headless_tests/events.rs b/packages/desktop/headless_tests/events.rs index b0aef5f79..4660441a2 100644 --- a/packages/desktop/headless_tests/events.rs +++ b/packages/desktop/headless_tests/events.rs @@ -231,12 +231,6 @@ fn app(cx: Scope) -> Element { assert!(event.data.held_buttons().is_empty()); assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary)); received_events.modify(|x| *x + 1) - }, - assert_eq!( - event.data.trigger_button(), - Some(dioxus_html::input_data::MouseButton::Primary), - ); - recieved_events.modify(|x| *x + 1) } } div { diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 96c763045..cfc5aeb5e 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -147,7 +147,6 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) _ => {} } - // Update the control flow *control_flow = app.control_flow; }) } From ec3eaa6b26dc36005a2e0d5fb42ee0d127450d49 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 16:48:53 -0800 Subject: [PATCH 06/43] Add ID to filedrop handler --- packages/desktop/headless_tests/rendering.rs | 2 +- packages/desktop/src/app.rs | 2 +- packages/desktop/src/cfg.rs | 6 ++-- packages/desktop/src/events.rs | 16 +++++----- packages/desktop/src/lib.rs | 15 ++++----- packages/desktop/src/protocol.rs | 32 ++------------------ packages/desktop/src/webview.rs | 2 +- 7 files changed, 24 insertions(+), 51 deletions(-) diff --git a/packages/desktop/headless_tests/rendering.rs b/packages/desktop/headless_tests/rendering.rs index 0907121d4..f228b79da 100644 --- a/packages/desktop/headless_tests/rendering.rs +++ b/packages/desktop/headless_tests/rendering.rs @@ -2,8 +2,8 @@ use dioxus::prelude::*; use dioxus_desktop::DesktopContext; pub(crate) fn check_app_exits(app: Component) { - use dioxus_desktop::tao::window::WindowBuilder; use dioxus_desktop::Config; + use tao::window::WindowBuilder; // This is a deadman's switch to ensure that the app exits let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); let should_panic_clone = should_panic.clone(); diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index 265991bea..59feb2f94 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -6,7 +6,7 @@ pub use crate::desktop_context::{ use crate::desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers}; use crate::element::DesktopElement; use crate::eval::init_eval; -use crate::events::{IpcMessage, KnownIpcMethod}; +use crate::events::{IpcMessage, IpcMethod}; use crate::file_upload; pub use crate::protocol::{ use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse, diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 9d1854bdf..4fb7f6ecb 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use std::path::PathBuf; -use tao::window::{Icon, Window, WindowBuilder}; +use tao::window::{Icon, WindowBuilder, WindowId}; use wry::{ http::{Request as HttpRequest, Response as HttpResponse}, FileDropEvent, @@ -37,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, @@ -119,7 +119,7 @@ impl Config { /// Set a file drop handler pub fn with_file_drop_handler( mut self, - handler: impl Fn(FileDropEvent) -> bool + 'static, + handler: impl Fn(WindowId, FileDropEvent) -> bool + 'static, ) -> Self { self.file_drop_handler = Some(Box::new(handler)); self diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index a877b52d6..e0a5a4319 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -9,7 +9,7 @@ pub struct IpcMessage { } #[derive(Deserialize, Serialize, Debug, Clone)] -pub enum KnownIpcMethod<'a> { +pub enum IpcMethod<'a> { FileDialog, UserEvent, Query, @@ -19,15 +19,15 @@ pub enum KnownIpcMethod<'a> { } impl IpcMessage { - pub(crate) fn method(&self) -> KnownIpcMethod { + pub(crate) fn method(&self) -> IpcMethod { 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), + "file_diolog" => IpcMethod::FileDialog, + "user_event" => IpcMethod::UserEvent, + "query" => IpcMethod::Query, + "browser_open" => IpcMethod::BrowserOpen, + "initialize" => IpcMethod::Initialize, + _ => IpcMethod::Other(&self.method), } } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index cfc5aeb5e..2e191ef8f 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -30,9 +30,10 @@ pub use desktop_context::{ }; use desktop_context::{EventData, UserWindowEvent}; use dioxus_core::*; -use events::KnownIpcMethod; +use events::IpcMethod; pub use protocol::{use_asset_handler, AssetFuture, AssetHandler, AssetRequest, AssetResponse}; pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError}; +pub use tao; pub use tao::dpi::{LogicalSize, PhysicalSize}; use tao::event::{Event, StartCause, WindowEvent}; pub use tao::window::WindowBuilder; @@ -136,12 +137,12 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) 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(_) => {} + 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(_) => {} }, }, _ => {} diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 6e28ca67c..d621d58d9 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -1,6 +1,5 @@ -use crate::{use_window, DesktopContext}; +use crate::DesktopContext; use dioxus_core::ScopeState; -use dioxus_interpreter_js::INTERPRETER_JS; use slab::Slab; use std::{ borrow::Cow, @@ -17,7 +16,7 @@ use tokio::{ }; use wry::{ http::{status::StatusCode, Request, Response}, - RequestAsyncResponder, Result, + Result, }; use crate::desktop_context::EditQueue; @@ -25,33 +24,6 @@ use crate::desktop_context::EditQueue; static MINIFIED: &str = include_str!("./minified.js"); fn module_loader(root_name: &str, headless: bool) -> 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(); - }); - } - } - }"#, - ); - format!( r#" -"# - ) -} - -/// An arbitrary asset is an HTTP response containing a binary body. -pub type AssetResponse = Response>; - -/// A future that returns an [`AssetResponse`]. This future may be spawned in a new thread, -/// so it must be [`Send`], [`Sync`], and `'static`. -pub trait AssetFuture: Future> + Send + Sync + 'static {} -impl> + Send + Sync + 'static> AssetFuture for T {} - -#[derive(Debug, Clone)] -/// A request for an asset. This is a wrapper around [`Request>`] that provides methods specific to asset requests. -pub struct AssetRequest { - path: PathBuf, - request: Arc>>, -} - -impl AssetRequest { - /// Get the path the asset request is for - pub fn path(&self) -> &Path { - &self.path - } -} - -impl From>> for AssetRequest { - fn from(request: Request>) -> Self { - let decoded = urlencoding::decode(request.uri().path().trim_start_matches('/')) - .expect("expected URL to be UTF-8 encoded"); - let path = PathBuf::from(&*decoded); - Self { - request: Arc::new(request), - path, - } - } -} - -impl Deref for AssetRequest { - type Target = Request>; - - fn deref(&self) -> &Self::Target { - &self.request - } -} - -/// A handler that takes an [`AssetRequest`] and returns a future that either loads the asset, or returns `None`. -/// This handler is stashed indefinitely in a context object, so it must be `'static`. -pub trait AssetHandler: Send + Sync + 'static { - /// Handle an asset request, returning a future that either loads the asset, or returns `None` - fn handle_request(&self, request: &AssetRequest) -> F; -} - -impl F + Send + Sync + 'static> AssetHandler for T { - fn handle_request(&self, request: &AssetRequest) -> F { - self(request) - } -} - -type AssetHandlerRegistryInner = - Slab Pin> + Send + Sync + 'static>>; - -#[derive(Clone)] -pub struct AssetHandlerRegistry(Arc>); - -impl AssetHandlerRegistry { - pub fn new() -> Self { - AssetHandlerRegistry(Arc::new(RwLock::new(Slab::new()))) - } - - pub async fn register_handler(&self, f: impl AssetHandler) -> usize { - let mut registry = self.0.write().await; - registry.insert(Box::new(move |req| Box::pin(f.handle_request(req)))) - } - - pub async fn remove_handler(&self, id: usize) -> Option<()> { - let mut registry = self.0.write().await; - registry.try_remove(id).map(|_| ()) - } - - pub async fn try_handlers(&self, req: &AssetRequest) -> Option { - let registry = self.0.read().await; - for (_, handler) in registry.iter() { - if let Some(response) = handler(req).await { - return Some(response); - } - } - None - } -} - -/// A handle to a registered asset handler. -pub struct AssetHandlerHandle { - desktop: DesktopContext, - handler_id: Rc>, -} - -impl AssetHandlerHandle { - /// Returns the ID for this handle. - /// - /// Because registering an ID is asynchronous, this may return `None` if the - /// registration has not completed yet. - pub fn handler_id(&self) -> Option { - self.handler_id.get().copied() - } -} - -impl Drop for AssetHandlerHandle { - fn drop(&mut self) { - let cell = Rc::clone(&self.handler_id); - let desktop = Rc::clone(&self.desktop); - tokio::task::block_in_place(move || { - Handle::current().block_on(async move { - if let Some(id) = cell.get() { - desktop.asset_handlers.remove_handler(*id).await; - } - }) - }); - } -} - -/// 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, - handler: impl AssetHandler, -) -> &AssetHandlerHandle { - cx.use_hook(|| { - let desktop = crate::window(); - let handler_id = Rc::new(OnceCell::new()); - let handler_id_ref = Rc::clone(&handler_id); - let desktop_ref = Rc::clone(&desktop); - cx.push_future(async move { - let id = desktop.asset_handlers.register_handler(handler).await; - handler_id.set(id).unwrap(); - }); - AssetHandlerHandle { - desktop: desktop_ref, - handler_id: handler_id_ref, - } - }) -} - pub(super) async fn desktop_handler( request: Request>, custom_head: Option, custom_index: Option, root_name: &str, asset_handlers: &AssetHandlerRegistry, - edit_queue: &EditQueue, + edit_queue: &crate::edits::EditQueue, headless: bool, responder: wry::RequestAsyncResponder, ) { @@ -293,6 +119,26 @@ pub(super) async fn desktop_handler( } } +fn module_loader(root_name: &str, headless: bool) -> String { + format!( + r#" + +"# + ) +} + #[allow(unreachable_code)] fn get_asset_root() -> Option { /* diff --git a/packages/desktop/src/waker.rs b/packages/desktop/src/waker.rs index 11f23d8d8..488c8f677 100644 --- a/packages/desktop/src/waker.rs +++ b/packages/desktop/src/waker.rs @@ -1,5 +1,5 @@ use crate::desktop_context::{EventData, UserWindowEvent}; -use futures_util::{pin_mut, task::ArcWake, FutureExt}; +use futures_util::task::ArcWake; use std::sync::Arc; use tao::{event_loop::EventLoopProxy, window::WindowId}; @@ -8,7 +8,7 @@ use tao::{event_loop::EventLoopProxy, window::WindowId}; /// 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, id: WindowId) -> std::task::Waker { +pub fn tao_waker(proxy: EventLoopProxy, id: WindowId) -> std::task::Waker { struct DomHandle { proxy: EventLoopProxy, id: WindowId, @@ -27,8 +27,5 @@ pub fn tao_waker(proxy: &EventLoopProxy, id: WindowId) -> std:: } } - futures_util::task::waker(Arc::new(DomHandle { - id, - proxy: proxy.clone(), - })) + futures_util::task::waker(Arc::new(DomHandle { id, proxy })) } diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 275678d73..537b3e241 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -1,26 +1,50 @@ -use crate::desktop_context::{EditQueue, EventData}; -use crate::protocol::{self, AssetHandlerRegistry}; -use crate::{desktop_context::UserWindowEvent, Config}; -use muda::{Menu, MenuItem, PredefinedMenuItem, Submenu}; +use std::{rc::Rc, task::Waker}; + +use crate::edits::{EditQueue, WebviewQueue}; +use crate::{ + assets::AssetHandlerRegistry, desktop_context::UserWindowEvent, waker::tao_waker, Config, + DesktopContext, +}; +use crate::{ + desktop_context::{EventData, WindowEventHandlers}, + eval::init_eval, + shortcut::ShortcutRegistry, +}; +use crate::{ + protocol::{self}, + DesktopService, +}; +use dioxus_core::VirtualDom; use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; -use tao::window::Window; -use wry::http::Response; -use wry::{WebContext, WebView, WebViewBuilder}; +use wry::{WebContext, WebViewBuilder}; -pub(crate) fn build( - cfg: &mut Config, +pub struct WebviewHandler { + pub dom: VirtualDom, + pub desktop_context: DesktopContext, + pub 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, +} + +pub fn create_new_window( + mut cfg: Config, event_loop: &EventLoopWindowTarget, - proxy: EventLoopProxy, -) -> (WebView, WebContext, AssetHandlerRegistry, EditQueue, Window) { - let mut builder = cfg.window.clone(); + proxy: &EventLoopProxy, + dom: VirtualDom, + queue: &WebviewQueue, + event_handlers: &WindowEventHandlers, + shortcut_manager: ShortcutRegistry, +) -> WebviewHandler { + let window = cfg.window.clone().build(event_loop).unwrap(); - // TODO: restore the menu bar with muda: https://github.com/tauri-apps/muda/blob/dev/examples/wry.rs + // TODO: allow users to specify their own menubars, again :/ if cfg.enable_default_menu_bar { - // builder = builder.with_menu(build_default_menu_bar()); + use crate::menubar::*; + build_menu_bar(build_default_menu_bar(), &window); } - let window = builder.build(event_loop).unwrap(); - let window_id = window.id(); let file_handler = cfg.file_drop_handler.take(); let custom_head = cfg.custom_head.clone(); @@ -49,10 +73,13 @@ pub(crate) fn build( .with_transparent(cfg.window.window.transparent) .with_url("dioxus://index.html/") .unwrap() - .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)); + .with_ipc_handler({ + let proxy = proxy.clone(); + 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)); + } } }) .with_asynchronous_custom_protocol(String::from("dioxus"), { @@ -101,76 +128,49 @@ pub(crate) fn build( webview = webview.with_custom_protocol(name, move |r| handler(r)) } + 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( - r#" - if (document.addEventListener) { - document.addEventListener('contextmenu', function(e) { - e.preventDefault(); - }, false); - } else { - document.attachEvent('oncontextmenu', function() { - window.event.returnValue = false; - }); - } - "#, - ) + 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); } - ( - webview.build().unwrap(), - web_context, - asset_handlers, - edit_queue, + let webview = webview.build().unwrap(); + + 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, + )); -/// 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(); + dom.base_scope().provide_context(desktop_context.clone()); - // 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(); + init_eval(dom.base_scope()); - 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 + WebviewHandler { + // We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both + waker: tao_waker(proxy.clone(), desktop_context.window.id()), + desktop_context, + dom, + _web_context: web_context, + } } From 6e08703c6c18f5d82cda66825ee76a78900244df Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 17:21:38 -0800 Subject: [PATCH 08/43] Fix compile issue --- packages/desktop/src/app.rs | 12 +++++++----- packages/desktop/src/desktop_context.rs | 6 +++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index 85e4eecbd..fac223ab0 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -334,12 +334,14 @@ impl App

{ let mut cx = std::task::Context::from_waker(&view.waker); loop { - let fut = view.dom.wait_for_work(); - pin_mut!(fut); + { + 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, + match fut.poll_unpin(&mut cx) { + std::task::Poll::Ready(_) => {} + std::task::Poll::Pending => break, + } } send_edits(view.dom.render_immediate(), &view.desktop_context); diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index af1204b06..866056558 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -34,6 +34,9 @@ pub fn window() -> DesktopContext { dioxus_core::prelude::consume_context().unwrap() } +/// A handle to the [`DesktopService`] that can be passed around. +pub type DesktopContext = Rc; + /// An imperative interface to the current window. /// /// To get a handle to the current window, use the [`use_window`] hook. @@ -78,9 +81,6 @@ pub struct DesktopService { pub(crate) views: Rc>>, } -/// A handle to the [`DesktopService`] that can be passed around. -pub type DesktopContext = Rc; - /// A smart pointer to the current window. impl std::ops::Deref for DesktopService { type Target = Window; From d47e49478666619d682d0bc9be5d2f43cc019a4d Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 17:28:54 -0800 Subject: [PATCH 09/43] More cleaning up of the desktop crate --- packages/desktop/src/app.rs | 11 +++-- packages/desktop/src/desktop_context.rs | 16 +++++++- packages/desktop/src/edits.rs | 54 +++++++------------------ packages/desktop/src/menubar.rs | 8 ++-- 4 files changed, 39 insertions(+), 50 deletions(-) diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index fac223ab0..931d239cb 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -2,7 +2,7 @@ pub use crate::assets::{AssetFuture, AssetHandler, AssetRequest, AssetResponse}; pub use crate::cfg::{Config, WindowCloseBehaviour}; pub use crate::desktop_context::DesktopContext; pub use crate::desktop_context::{window, DesktopService, WryEventHandler, WryEventHandlerId}; -use crate::edits::{send_edits, EditQueue, WebviewQueue}; +use crate::edits::{EditQueue, WebviewQueue}; use crate::element::DesktopElement; use crate::eval::init_eval; use crate::events::{IpcMessage, IpcMethod}; @@ -198,7 +198,7 @@ impl App

{ 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.send_edits(view.dom.rebuild()); view.desktop_context .window .set_visible(self.is_visible_before_start); @@ -271,8 +271,7 @@ impl App

{ }; view.dom.handle_event(&name, as_any, element, bubbles); - - send_edits(view.dom.render_immediate(), &view.desktop_context); + view.desktop_context.send_edits(view.dom.render_immediate()); } #[cfg(all(feature = "hot-reload", debug_assertions))] @@ -319,7 +318,7 @@ impl App

{ view.dom.handle_event(event_name, data, id, event_bubbles); } - send_edits(view.dom.render_immediate(), &view.desktop_context); + view.desktop_context.send_edits(view.dom.render_immediate()); } } @@ -344,7 +343,7 @@ impl App

{ } } - send_edits(view.dom.render_immediate(), &view.desktop_context); + view.desktop_context.send_edits(view.dom.render_immediate()); } } } diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 866056558..0a2a768e4 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -5,8 +5,8 @@ use crate::Config; use crate::{assets::AssetFuture, edits::WebviewQueue}; use crate::{assets::AssetHandlerRegistry, edits::EditQueue}; use crate::{events::IpcMessage, webview::WebviewHandler}; -use dioxus_core::ScopeState; use dioxus_core::VirtualDom; +use dioxus_core::{Mutations, ScopeState}; use dioxus_interpreter_js::binary_protocol::Channel; use rustc_hash::FxHashMap; use slab::Slab; @@ -121,6 +121,20 @@ impl DesktopService { } } + /// Send a list of mutations to the webview + pub(crate) fn send_edits(&self, edits: Mutations) { + let mut channel = self.channel.borrow_mut(); + let mut templates = self.templates.borrow_mut(); + if let Some(bytes) = crate::edits::apply_edits( + edits, + &mut channel, + &mut templates, + &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. diff --git a/packages/desktop/src/edits.rs b/packages/desktop/src/edits.rs index 5a09d1933..fa9306156 100644 --- a/packages/desktop/src/edits.rs +++ b/packages/desktop/src/edits.rs @@ -1,24 +1,21 @@ -use crate::assets::AssetHandlerRegistry; use crate::query::QueryEngine; use crate::shortcut::{HotKey, ShortcutId, ShortcutRegistry, ShortcutRegistryError}; use crate::AssetHandler; use crate::Config; use crate::{assets::AssetFuture, DesktopContext}; +use crate::{assets::AssetHandlerRegistry, DesktopService}; use crate::{events::IpcMessage, webview::WebviewHandler}; use dioxus_core::{BorrowedAttributeValue, Template, TemplateAttribute, TemplateNode, VirtualDom}; use dioxus_core::{Mutations, ScopeState}; use dioxus_html::event_bubbles; use dioxus_interpreter_js::binary_protocol::Channel; use rustc_hash::FxHashMap; -use slab::Slab; 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}, + cell::RefCell, + rc::Rc, + sync::atomic::AtomicU16, + sync::Arc, + sync::{atomic::Ordering, Mutex}, }; use wry::{RequestAsyncResponder, WebView}; @@ -33,17 +30,6 @@ pub(crate) struct EditQueue { responder: Arc>>, } -impl Debug for EditQueue { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("EditQueue") - .field("queue", &self.queue) - .field("responder", { - &self.responder.lock().unwrap().as_ref().map(|_| ()) - }) - .finish() - } -} - impl EditQueue { pub fn handle_request(&self, responder: RequestAsyncResponder) { let mut queue = self.queue.lock().unwrap(); @@ -64,33 +50,21 @@ impl EditQueue { } } -/// 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 fn apply_edits( +pub(crate) 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); } + + use dioxus_core::Mutation::*; for edit in mutations.edits { match edit { AppendChildren { id, m } => channel.append_children(id.0 as u32, m as u16), @@ -156,19 +130,19 @@ pub fn add_template( templates: &mut FxHashMap, max_template_count: &AtomicU16, ) { - let current_max_template_count = max_template_count.load(std::sync::atomic::Ordering::Relaxed); + 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, std::sync::atomic::Ordering::Relaxed); + max_template_count.fetch_add(1, Ordering::Relaxed); } -pub fn create_template_node(channel: &mut Channel, v: &'static TemplateNode<'static>) { +pub fn create_template_node(channel: &mut Channel, node: &'static TemplateNode<'static>) { use TemplateNode::*; - match v { + match node { Element { tag, namespace, diff --git a/packages/desktop/src/menubar.rs b/packages/desktop/src/menubar.rs index 44cc12c84..2f7f3951a 100644 --- a/packages/desktop/src/menubar.rs +++ b/packages/desktop/src/menubar.rs @@ -1,12 +1,14 @@ -use muda::{Menu, MenuItem, PredefinedMenuItem, Submenu}; +use muda::{Menu, PredefinedMenuItem, Submenu}; use tao::window::Window; +#[allow(unused)] pub fn build_menu_bar(menu: Menu, window: &Window) { #[cfg(target_os = "windows")] - menu.init_for_hwnd(window_hwnd); + menu.init_for_hwnd(window); #[cfg(target_os = "linux")] - menu.init_for_gtk_window(>k_window, Some(&vertical_gtk_box)); + menu.init_for_gtk_window(window, None); + // menu.init_for_gtk_window(window, Some(&vertical_gtk_box)); #[cfg(target_os = "macos")] menu.init_for_nsapp(); From fef7b83b792a5e6db1563c1a9fd70d464acf7e4e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 17:35:58 -0800 Subject: [PATCH 10/43] Rename editqueue type --- packages/desktop/src/protocol.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 6910495f0..77acebad6 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -1,4 +1,4 @@ -use crate::assets::*; +use crate::{assets::*, edits::EditQueue}; use std::{ borrow::Cow, path::{Path, PathBuf}, @@ -16,7 +16,7 @@ pub(super) async fn desktop_handler( custom_index: Option, root_name: &str, asset_handlers: &AssetHandlerRegistry, - edit_queue: &crate::edits::EditQueue, + edit_queue: &EditQueue, headless: bool, responder: wry::RequestAsyncResponder, ) { From 7444796a85646605e56a063bc4c1116d82fe490f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 18:01:13 -0800 Subject: [PATCH 11/43] Simplify asset code a bit --- packages/desktop/src/app.rs | 8 +-- packages/desktop/src/assets.rs | 6 +- packages/desktop/src/desktop_context.rs | 27 +++----- packages/desktop/src/edits.rs | 22 +++--- packages/desktop/src/hooks.rs | 20 +++++- packages/desktop/src/lib.rs | 6 +- packages/desktop/src/shortcut.rs | 89 ++++++++++--------------- packages/desktop/src/webview.rs | 2 +- 8 files changed, 84 insertions(+), 96 deletions(-) diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index 931d239cb..4e954d496 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -11,7 +11,7 @@ use crate::hooks::*; use crate::query::QueryResult; use crate::shortcut::GlobalHotKeyEvent; use crate::shortcut::ShortcutRegistry; -pub use crate::shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError}; +pub use crate::shortcut::{ShortcutHandle, ShortcutId, ShortcutRegistryError}; use crate::{ desktop_context::{EventData, UserWindowEvent, WindowEventHandlers}, webview::WebviewHandler, @@ -171,9 +171,9 @@ impl App

{ let handler = crate::webview::create_new_window( cfg, + dom, target, &self.proxy, - dom, &self.queue, &self.event_handlers, self.shortcut_manager.clone(), @@ -212,10 +212,6 @@ impl App

{ } } - 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; diff --git a/packages/desktop/src/assets.rs b/packages/desktop/src/assets.rs index f27dcb504..f80c7542a 100644 --- a/packages/desktop/src/assets.rs +++ b/packages/desktop/src/assets.rs @@ -75,8 +75,10 @@ impl F + Send + Sync + 'static> AssetHan } } -type AssetHandlerRegistryInner = - Slab Pin> + Send + Sync + 'static>>; +type UserAssetHandler = + Box Pin> + Send + Sync + 'static>; + +type AssetHandlerRegistryInner = Slab; #[derive(Clone)] pub struct AssetHandlerRegistry(Arc>); diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 0a2a768e4..240f8ec24 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,26 +1,22 @@ +use crate::events::IpcMessage; use crate::query::QueryEngine; use crate::shortcut::{HotKey, ShortcutId, ShortcutRegistry, ShortcutRegistryError}; use crate::AssetHandler; use crate::Config; use crate::{assets::AssetFuture, edits::WebviewQueue}; use crate::{assets::AssetHandlerRegistry, edits::EditQueue}; -use crate::{events::IpcMessage, webview::WebviewHandler}; +use dioxus_core::Mutations; use dioxus_core::VirtualDom; -use dioxus_core::{Mutations, ScopeState}; use dioxus_interpreter_js::binary_protocol::Channel; use rustc_hash::FxHashMap; use slab::Slab; -use std::{ - cell::RefCell, fmt::Debug, fmt::Formatter, rc::Rc, rc::Weak, sync::atomic::AtomicU16, - sync::Arc, sync::Mutex, -}; +use std::{cell::RefCell, fmt::Debug, rc::Rc, rc::Weak, sync::atomic::AtomicU16}; use tao::{ event::Event, event_loop::{EventLoopProxy, EventLoopWindowTarget}, window::{Fullscreen as WryFullscreen, Window, WindowId}, }; - -use wry::{RequestAsyncResponder, WebView}; +use wry::WebView; #[cfg(target_os = "ios")] use tao::platform::ios::WindowExtIOS; @@ -123,16 +119,13 @@ impl DesktopService { /// Send a list of mutations to the webview pub(crate) fn send_edits(&self, edits: Mutations) { - let mut channel = self.channel.borrow_mut(); - let mut templates = self.templates.borrow_mut(); - if let Some(bytes) = crate::edits::apply_edits( + crate::edits::apply_edits( edits, - &mut channel, - &mut templates, + &mut self.channel.borrow_mut(), + &mut self.templates.borrow_mut(), &self.max_template_count, - ) { - self.edit_queue.add_edits(bytes) - } + ) + .map(|bytes| self.edit_queue.add_edits(bytes)); } /// Create a new window using the props and window builder @@ -145,9 +138,9 @@ impl DesktopService { pub fn new_window(&self, dom: VirtualDom, cfg: Config) -> Weak { let window = crate::webview::create_new_window( cfg, + dom, &self.event_loop, &self.proxy, - dom, &self.pending_windows, &self.event_handlers, self.shortcut_manager.clone(), diff --git a/packages/desktop/src/edits.rs b/packages/desktop/src/edits.rs index fa9306156..4eeb48a9b 100644 --- a/packages/desktop/src/edits.rs +++ b/packages/desktop/src/edits.rs @@ -1,12 +1,6 @@ -use crate::query::QueryEngine; -use crate::shortcut::{HotKey, ShortcutId, ShortcutRegistry, ShortcutRegistryError}; -use crate::AssetHandler; -use crate::Config; -use crate::{assets::AssetFuture, DesktopContext}; -use crate::{assets::AssetHandlerRegistry, DesktopService}; -use crate::{events::IpcMessage, webview::WebviewHandler}; -use dioxus_core::{BorrowedAttributeValue, Template, TemplateAttribute, TemplateNode, VirtualDom}; -use dioxus_core::{Mutations, ScopeState}; +use crate::webview::WebviewHandler; +use dioxus_core::Mutations; +use dioxus_core::{BorrowedAttributeValue, Template, TemplateAttribute, TemplateNode}; use dioxus_html::event_bubbles; use dioxus_interpreter_js::binary_protocol::Channel; use rustc_hash::FxHashMap; @@ -18,12 +12,16 @@ use std::{ sync::{atomic::Ordering, Mutex}, }; -use wry::{RequestAsyncResponder, WebView}; +use wry::RequestAsyncResponder; pub(crate) type WebviewQueue = Rc>>; -/// 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. +/// 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>>>, diff --git a/packages/desktop/src/hooks.rs b/packages/desktop/src/hooks.rs index 7ebdf4963..54d114abb 100644 --- a/packages/desktop/src/hooks.rs +++ b/packages/desktop/src/hooks.rs @@ -1,6 +1,6 @@ use std::rc::Rc; -use crate::assets::*; +use crate::{assets::*, shortcut::IntoAccelerator, ShortcutHandle, ShortcutRegistryError}; use crate::{desktop_context::UserWindowEvent, window, DesktopContext, WryEventHandler}; use dioxus_core::ScopeState; use tao::{event::Event, event_loop::EventLoopWindowTarget}; @@ -52,3 +52,21 @@ pub fn use_asset_handler( } }) } + +/// 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 { + cx.use_hook(move || { + let desktop = window(); + + let id = desktop.create_shortcut(accelerator.accelerator(), handler); + + Ok(ShortcutHandle { + desktop, + shortcut_id: id?, + }) + }) +} diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 3e8608200..ca1e851fb 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -37,9 +37,9 @@ pub use cfg::{Config, WindowCloseBehaviour}; pub use desktop_context::{ window, DesktopContext, DesktopService, WryEventHandler, WryEventHandlerId, }; -pub use hooks::{use_asset_handler, use_window, use_wry_event_handler}; +pub use hooks::{use_asset_handler, use_global_shortcut, use_window, use_wry_event_handler}; pub use menubar::build_default_menu_bar; -pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError}; +pub use shortcut::{ShortcutHandle, ShortcutId, ShortcutRegistryError}; #[allow(deprecated)] use desktop_context::{EventData, UserWindowEvent}; @@ -139,7 +139,7 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) _ => {} }, Event::UserEvent(UserWindowEvent(event, id)) => match event { - EventData::Poll => app.handle_poll_msg(id), + 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), diff --git a/packages/desktop/src/shortcut.rs b/packages/desktop/src/shortcut.rs index 55d49b405..f67d21a4d 100644 --- a/packages/desktop/src/shortcut.rs +++ b/packages/desktop/src/shortcut.rs @@ -1,11 +1,10 @@ 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 tao::keyboard::ModifiersState; -use crate::{desktop_context::DesktopContext, window}; +use crate::desktop_context::DesktopContext; #[cfg(any( target_os = "windows", @@ -26,7 +25,7 @@ pub use crate::mobile_shortcut::*; #[derive(Clone)] pub(crate) struct ShortcutRegistry { - manager: Rc>, + manager: Rc, shortcuts: ShortcutMap, } @@ -55,7 +54,7 @@ impl Shortcut { impl ShortcutRegistry { pub fn new() -> Self { Self { - manager: Rc::new(RefCell::new(GlobalHotKeyManager::new().unwrap())), + manager: Rc::new(GlobalHotKeyManager::new().unwrap()), shortcuts: Rc::new(RefCell::new(HashMap::new())), } } @@ -74,36 +73,36 @@ impl ShortcutRegistry { callback: Box, ) -> Result { 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(hotkey) { - Ok(_) => { - let mut slab = Slab::new(); - let id = slab.insert(callback); - let shortcut = Shortcut { - shortcut: hotkey, - callbacks: slab, - }; - shortcuts.insert(accelerator_id, shortcut); - ShortcutId { - id: accelerator_id, - number: id, - } - } - Err(HotkeyError::HotKeyParseError(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) { @@ -112,7 +111,7 @@ impl ShortcutRegistry { callbacks.remove(id.number); if callbacks.is_empty() { if let Some(_shortcut) = shortcuts.remove(&id.id) { - let _ = self.manager.borrow_mut().unregister(_shortcut.shortcut); + let _ = self.manager.unregister(_shortcut.shortcut); } } } @@ -121,7 +120,7 @@ impl ShortcutRegistry { pub(crate) fn remove_all(&self) { let mut shortcuts = self.shortcuts.borrow_mut(); let hotkeys: Vec<_> = shortcuts.drain().map(|(_, v)| v.shortcut).collect(); - let _ = self.manager.borrow_mut().unregister_all(&hotkeys); + let _ = self.manager.unregister_all(&hotkeys); } } @@ -144,7 +143,7 @@ pub struct ShortcutId { /// 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, } @@ -177,24 +176,6 @@ impl IntoAccelerator for &str { } } -/// 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 { - cx.use_hook(move || { - let desktop = window(); - - let id = desktop.create_shortcut(accelerator.accelerator(), handler); - - Ok(ShortcutHandle { - desktop, - shortcut_id: id?, - }) - }) -} - impl ShortcutHandle { /// Remove the shortcut. pub fn remove(&self) { diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 537b3e241..cd83362b5 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -30,9 +30,9 @@ pub struct WebviewHandler { pub fn create_new_window( mut cfg: Config, + dom: VirtualDom, event_loop: &EventLoopWindowTarget, proxy: &EventLoopProxy, - dom: VirtualDom, queue: &WebviewQueue, event_handlers: &WindowEventHandlers, shortcut_manager: ShortcutRegistry, From 060490892ac6d3ff7d6dc1362d1583b46e3d817e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 18:19:28 -0800 Subject: [PATCH 12/43] Clean up desktop even more --- packages/desktop/src/app.rs | 142 +++++++++--------------- packages/desktop/src/assets.rs | 7 +- packages/desktop/src/desktop_context.rs | 14 +-- packages/desktop/src/eval.rs | 16 ++- packages/desktop/src/file_upload.rs | 125 +++++++++++---------- packages/desktop/src/webview.rs | 11 +- 6 files changed, 137 insertions(+), 178 deletions(-) diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index 4e954d496..9a45e6c19 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -1,48 +1,25 @@ -pub use crate::assets::{AssetFuture, AssetHandler, AssetRequest, AssetResponse}; -pub use crate::cfg::{Config, WindowCloseBehaviour}; -pub use crate::desktop_context::DesktopContext; -pub use crate::desktop_context::{window, DesktopService, WryEventHandler, WryEventHandlerId}; -use crate::edits::{EditQueue, WebviewQueue}; -use crate::element::DesktopElement; -use crate::eval::init_eval; -use crate::events::{IpcMessage, IpcMethod}; -use crate::file_upload; -use crate::hooks::*; -use crate::query::QueryResult; -use crate::shortcut::GlobalHotKeyEvent; -use crate::shortcut::ShortcutRegistry; -pub use crate::shortcut::{ShortcutHandle, ShortcutId, ShortcutRegistryError}; use crate::{ + cfg::{Config, WindowCloseBehaviour}, desktop_context::{EventData, UserWindowEvent, WindowEventHandlers}, + edits::WebviewQueue, + element::DesktopElement, + events::IpcMessage, + file_upload::FileDialogRequest, + query::QueryResult, + shortcut::{GlobalHotKeyEvent, ShortcutRegistry}, webview::WebviewHandler, }; -use dioxus_core::*; -use dioxus_html::{event_bubbles, MountedData}; +use dioxus_core::{Component, ElementId, VirtualDom}; +use dioxus_html::MountedData; use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent}; -use dioxus_interpreter_js::binary_protocol::Channel; use futures_util::{pin_mut, FutureExt}; -use global_hotkey::{ - hotkey::{Code, HotKey, Modifiers}, - GlobalHotKeyManager, -}; -use rustc_hash::FxHashMap; +use std::cell::Cell; 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::EventLoopBuilder; 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 tao::{event::Event, event_loop::ControlFlow}; use wry::WebView; pub struct App

{ @@ -221,24 +198,14 @@ impl App

{ return; }; - view.dom - .base_scope() - .consume_context::() - .unwrap() - .query - .send(result); + view.desktop_context.query.send(result); } pub fn handle_user_event_msg(&mut self, msg: IpcMessage, id: WindowId) { - let params = msg.params(); + let parsed_params = serde_json::from_value(msg.params()) + .map_err(|err| tracing::error!("Error parsing user_event: {:?}", err)); - let evt = match serde_json::from_value::(params) { - Ok(value) => value, - Err(err) => { - tracing::error!("Error parsing user_event: {:?}", err); - return; - } - }; + let Ok(evt) = parsed_params else { return }; let HtmlEvent { element, @@ -250,20 +217,13 @@ impl App

{ 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() + let as_any = match data { + dioxus_html::EventData::Mounted => Rc::new(MountedData::new(DesktopElement::new( + element, + view.desktop_context.clone(), + view.desktop_context.query.clone(), + ))), + _ => data.into_any(), }; view.dom.handle_event(&name, as_any, element, bubbles); @@ -278,9 +238,7 @@ impl App

{ webview.dom.replace_template(template); } - let ids = self.webviews.keys().copied().collect::>(); - - for id in ids { + for id in self.webviews.keys().copied().collect::>() { self.poll_vdom(id); } } @@ -291,31 +249,32 @@ impl App

{ } pub fn handle_file_dialog_msg(&mut 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 Ok(file_dialog) = serde_json::from_value::(msg.params()) else { + return; + }; - let view = self.webviews.get_mut(&window).unwrap(); + 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(); - 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); - } + let data = Rc::new(FormData { + value: Default::default(), + values: Default::default(), + files: Some(Arc::new(NativeFileEngine::new(files))), + }); - view.desktop_context.send_edits(view.dom.render_immediate()); + 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 @@ -324,10 +283,15 @@ impl App

{ /// /// 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 Some(view) = self.webviews.get_mut(&id) else { + return; + }; let mut cx = std::task::Context::from_waker(&view.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 = view.dom.wait_for_work(); @@ -335,7 +299,7 @@ impl App

{ match fut.poll_unpin(&mut cx) { std::task::Poll::Ready(_) => {} - std::task::Poll::Pending => break, + std::task::Poll::Pending => return, } } diff --git a/packages/desktop/src/assets.rs b/packages/desktop/src/assets.rs index f80c7542a..7f86b6184 100644 --- a/packages/desktop/src/assets.rs +++ b/packages/desktop/src/assets.rs @@ -1,6 +1,4 @@ -use crate::edits::EditQueue; use crate::DesktopContext; -use dioxus_core::ScopeState; use slab::Slab; use std::{ borrow::Cow, @@ -15,10 +13,7 @@ use tokio::{ runtime::Handle, sync::{OnceCell, RwLock}, }; -use wry::{ - http::{status::StatusCode, Request, Response}, - Result, -}; +use wry::http::{Request, Response}; /// An arbitrary asset is an HTTP response containing a binary body. pub type AssetResponse = Response>; diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 240f8ec24..ef2f671b2 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -146,25 +146,19 @@ impl DesktopService { self.shortcut_manager.clone(), ); - let desktop_context = window - .dom - .base_scope() - .consume_context::>() - .unwrap(); - - let id = window.desktop_context.window.id(); + let cx = window.desktop_context.clone(); self.proxy - .send_event(UserWindowEvent(EventData::NewWindow, id)) + .send_event(UserWindowEvent(EventData::NewWindow, cx.id())) .unwrap(); self.proxy - .send_event(UserWindowEvent(EventData::Poll, id)) + .send_event(UserWindowEvent(EventData::Poll, cx.id())) .unwrap(); self.pending_windows.borrow_mut().push(window); - Rc::downgrade(&desktop_context) + Rc::downgrade(&cx) } /// trigger the drag-window event diff --git a/packages/desktop/src/eval.rs b/packages/desktop/src/eval.rs index e187be450..e9b0f036b 100644 --- a/packages/desktop/src/eval.rs +++ b/packages/desktop/src/eval.rs @@ -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::().unwrap(); - let provider: Rc = 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 { diff --git a/packages/desktop/src/file_upload.rs b/packages/desktop/src/file_upload.rs index 7b5f08fc3..98dbec437 100644 --- a/packages/desktop/src/file_upload.rs +++ b/packages/desktop/src/file_upload.rs @@ -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 { - 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 { + 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 { - fn get_file_event_for_folder( - request: &FileDialogRequest, - dialog: rfd::FileDialog, - ) -> Vec { - 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 { + fn get_file_event_for_folder( + request: &FileDialogRequest, + dialog: rfd::FileDialog, + ) -> Vec { + 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 { - 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 { + 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) + } } } diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index cd83362b5..6eac25b62 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -1,15 +1,17 @@ use std::{rc::Rc, task::Waker}; -use crate::edits::{EditQueue, WebviewQueue}; use crate::{ assets::AssetHandlerRegistry, desktop_context::UserWindowEvent, waker::tao_waker, Config, DesktopContext, }; use crate::{ desktop_context::{EventData, WindowEventHandlers}, - eval::init_eval, shortcut::ShortcutRegistry, }; +use crate::{ + edits::{EditQueue, WebviewQueue}, + eval::DesktopEvalProvider, +}; use crate::{ protocol::{self}, DesktopService, @@ -162,9 +164,12 @@ pub fn create_new_window( asset_handlers, )); + // Provide the desktop context to the virtualdom dom.base_scope().provide_context(desktop_context.clone()); - init_eval(dom.base_scope()); + // Also set up its eval provider + dom.base_scope() + .provide_context(Rc::new(DesktopEvalProvider::new(desktop_context.clone()))); WebviewHandler { // We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both From 73d5069a2056c0a2ea4a0d88e00c91a7e0d12185 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 18:31:22 -0800 Subject: [PATCH 13/43] Move launch functions into their own file --- packages/desktop/src/launch.rs | 120 ++++++++++++++++++++++++++++++ packages/desktop/src/lib.rs | 130 ++------------------------------- 2 files changed, 126 insertions(+), 124 deletions(-) create mode 100644 packages/desktop/src/launch.rs diff --git a/packages/desktop/src/launch.rs b/packages/desktop/src/launch.rs new file mode 100644 index 000000000..8af51e137 --- /dev/null +++ b/packages/desktop/src/launch.rs @@ -0,0 +1,120 @@ +use crate::{ + app::App, + desktop_context::{EventData, UserWindowEvent}, + events::IpcMethod, + Config, +}; +use dioxus_core::*; +use tao::event::{Event, StartCause, WindowEvent}; +use tokio::runtime::Builder; + +/// 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. +/// +/// 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) -> Element { +/// cx.render(rsx!{ +/// h1 {"hello {cx.props.name}!"} +/// }) +/// } +/// ``` +pub fn launch_with_props(root: Component

, props: P, cfg: Config) { + // We start the tokio runtime *on this thread* + // Any future we poll later will use this runtime to spawn tasks and for IO + // I would love to just allow dioxus to work with any runtime... but tokio is weird + let rt = Builder::new_multi_thread().enable_all().build().unwrap(); + let _guard = rt.enter(); + + let (event_loop, mut app) = App::new(cfg, props, root); + + event_loop.run(move |window_event, event_loop, control_flow| { + 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 => 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; + }) +} diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index ca1e851fb..818605dc5 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -23,12 +23,12 @@ mod shortcut; mod waker; mod webview; -// Re-exports -pub use tao::{ - self, - dpi::{LogicalSize, PhysicalSize}, - window::WindowBuilder, -}; +// 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 wry; // Public exports @@ -40,121 +40,3 @@ pub use desktop_context::{ pub use hooks::{use_asset_handler, use_global_shortcut, use_window, use_wry_event_handler}; pub use menubar::build_default_menu_bar; pub use shortcut::{ShortcutHandle, ShortcutId, ShortcutRegistryError}; - -#[allow(deprecated)] -use desktop_context::{EventData, UserWindowEvent}; -use dioxus_core::*; -use events::IpcMethod; -use tao::event::{Event, StartCause, WindowEvent}; -use tokio::runtime::Builder; - -/// 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. -/// -/// 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) -> Element { -/// cx.render(rsx!{ -/// h1 {"hello {cx.props.name}!"} -/// }) -/// } -/// ``` -pub fn launch_with_props(root: Component

, props: P, cfg: Config) { - // We start the tokio runtime *on this thread* - // Any future we poll later will use this runtime to spawn tasks and for IO - // I would love to just allow dioxus to work with any runtime... but tokio is weird - let rt = &Builder::new_multi_thread().enable_all().build().unwrap(); - let _guard = rt.enter(); - - let (event_loop, mut app) = app::App::new(cfg, props, root); - - event_loop.run(move |window_event, event_loop, control_flow| { - 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 => 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; - }) -} From 38216fa2729a8f07fcb055d60dc66bebdd6db908 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 19:09:59 -0800 Subject: [PATCH 14/43] Make the tokio runtime optional --- examples/clock.rs | 5 +++-- packages/desktop/src/app.rs | 24 +++++++++----------- packages/desktop/src/cfg.rs | 15 +++++++++++++ packages/desktop/src/desktop_context.rs | 2 +- packages/desktop/src/{events.rs => ipc.rs} | 0 packages/desktop/src/launch.rs | 26 ++++++++++++++++------ packages/desktop/src/lib.rs | 7 +++--- packages/desktop/src/protocol.rs | 9 ++++++-- 8 files changed, 60 insertions(+), 28 deletions(-) rename packages/desktop/src/{events.rs => ipc.rs} (100%) diff --git a/examples/clock.rs b/examples/clock.rs index f9c1892d9..4b7be3d3b 100644 --- a/examples/clock.rs +++ b/examples/clock.rs @@ -1,7 +1,8 @@ use dioxus::prelude::*; use dioxus_signals::use_signal; -fn main() { +#[tokio::main] +async fn main() { dioxus_desktop::launch(app); } @@ -10,7 +11,7 @@ fn app(cx: Scope) -> Element { use_future!(cx, || async move { loop { - tokio::time::sleep(std::time::Duration::from_millis(100)).await; + tokio::time::sleep(std::time::Duration::from_millis(10)).await; count += 1; println!("current: {count}"); } diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index 9a45e6c19..c8ca8a92a 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -3,30 +3,28 @@ use crate::{ desktop_context::{EventData, UserWindowEvent, WindowEventHandlers}, edits::WebviewQueue, element::DesktopElement, - events::IpcMessage, file_upload::FileDialogRequest, + ipc::IpcMessage, query::QueryResult, shortcut::{GlobalHotKeyEvent, ShortcutRegistry}, webview::WebviewHandler, }; use dioxus_core::{Component, ElementId, VirtualDom}; -use dioxus_html::MountedData; -use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent}; +use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent, MountedData}; use futures_util::{pin_mut, FutureExt}; -use std::cell::Cell; -use std::rc::Rc; -use std::{collections::HashMap, sync::Arc}; -use tao::event_loop::EventLoopBuilder; -use tao::event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}; -use tao::window::WindowId; -use tao::{event::Event, event_loop::ControlFlow}; -use wry::WebView; +use std::{cell::Cell, collections::HashMap, rc::Rc, sync::Arc}; +use tao::{ + event::Event, + event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget}, + window::WindowId, +}; -pub struct App

{ +pub(crate) 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, @@ -310,7 +308,7 @@ impl App

{ /// Different hide implementations per platform #[allow(unused)] -pub fn hide_app_window(webview: &WebView) { +pub fn hide_app_window(webview: &wry::WebView) { #[cfg(target_os = "windows")] { use wry::application::platform::windows::WindowExtWindows; diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 4fb7f6ecb..a5d17b541 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; use std::path::PathBuf; +use dioxus_core::prelude::Component; use tao::window::{Icon, WindowBuilder, WindowId}; use wry::{ http::{Request as HttpRequest, Response as HttpResponse}, @@ -68,6 +69,20 @@ impl Config { } } + /// 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(self, root: Component

, 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`. diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index ef2f671b2..e1ed1f07a 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,4 +1,4 @@ -use crate::events::IpcMessage; +use crate::ipc::IpcMessage; use crate::query::QueryEngine; use crate::shortcut::{HotKey, ShortcutId, ShortcutRegistry, ShortcutRegistryError}; use crate::AssetHandler; diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/ipc.rs similarity index 100% rename from packages/desktop/src/events.rs rename to packages/desktop/src/ipc.rs diff --git a/packages/desktop/src/launch.rs b/packages/desktop/src/launch.rs index 8af51e137..8ee939494 100644 --- a/packages/desktop/src/launch.rs +++ b/packages/desktop/src/launch.rs @@ -1,12 +1,11 @@ use crate::{ app::App, desktop_context::{EventData, UserWindowEvent}, - events::IpcMethod, + ipc::IpcMethod, Config, }; use dioxus_core::*; use tao::event::{Event, StartCause, WindowEvent}; -use tokio::runtime::Builder; /// Launch the WebView and run the event loop. /// @@ -55,6 +54,7 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// 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 @@ -78,12 +78,24 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// } /// ``` pub fn launch_with_props(root: Component

, props: P, cfg: Config) { - // We start the tokio runtime *on this thread* - // Any future we poll later will use this runtime to spawn tasks and for IO - // I would love to just allow dioxus to work with any runtime... but tokio is weird - let rt = Builder::new_multi_thread().enable_all().build().unwrap(); - let _guard = rt.enter(); + #[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(root: Component

, props: P, cfg: Config) { let (event_loop, mut app) = App::new(cfg, props, root); event_loop.run(move |window_event, event_loop, control_flow| { diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 818605dc5..3f8d9d3ce 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -11,18 +11,19 @@ mod edits; mod element; mod escape; mod eval; -mod events; mod file_upload; mod hooks; +mod ipc; mod menubar; -#[cfg(any(target_os = "ios", target_os = "android"))] -mod mobile_shortcut; mod protocol; mod query; mod shortcut; mod waker; mod webview; +#[cfg(any(target_os = "ios", target_os = "android"))] +mod mobile_shortcut; + // The main entrypoint for this crate pub use launch::*; mod launch; diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 77acebad6..bdcefa473 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -57,7 +57,8 @@ pub(super) async fn desktop_handler( .body(Cow::from(body)) { Ok(response) => { - return responder.respond(response); + responder.respond(response); + return; } Err(err) => tracing::error!("error building response: {}", err), } @@ -103,7 +104,10 @@ pub(super) async fn desktop_handler( .header("Content-Type", content_type) .body(Cow::from(asset)) { - Ok(response) => return responder.respond(response), + Ok(response) => { + responder.respond(response); + return; + } Err(err) => tracing::error!("error building response: {}", err), } } @@ -114,6 +118,7 @@ pub(super) async fn desktop_handler( { Ok(response) => { responder.respond(response); + return; } Err(err) => tracing::error!("error building response: {}", err), } From 2e9737ef57ad37a8eb7444ebe3b32aa6c3fbf75f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 Jan 2024 19:52:49 -0800 Subject: [PATCH 15/43] Clean up and document the protocol handler --- examples/clock.rs | 3 +- packages/desktop/src/index.html | 18 +-- packages/desktop/src/protocol.rs | 199 ++++++++++++++----------------- 3 files changed, 99 insertions(+), 121 deletions(-) diff --git a/examples/clock.rs b/examples/clock.rs index 4b7be3d3b..ba47d994a 100644 --- a/examples/clock.rs +++ b/examples/clock.rs @@ -1,8 +1,7 @@ use dioxus::prelude::*; use dioxus_signals::use_signal; -#[tokio::main] -async fn main() { +fn main() { dioxus_desktop::launch(app); } diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index 0704fd0f0..45926e3f1 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -1,12 +1,12 @@ - - Dioxus app - - - - -

- - + + Dioxus app + + + + +
+ + diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index bdcefa473..be3c12231 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -5,10 +5,11 @@ use std::{ }; use wry::{ http::{status::StatusCode, Request, Response}, - Result, + RequestAsyncResponder, Result, }; static MINIFIED: &str = include_str!("./minified.js"); +static DEFAULT_INDEX: &str = include_str!("./index.html"); pub(super) async fn desktop_handler( request: Request>, @@ -18,120 +19,110 @@ pub(super) async fn desktop_handler( asset_handlers: &AssetHandlerRegistry, edit_queue: &EditQueue, headless: bool, - responder: wry::RequestAsyncResponder, + responder: RequestAsyncResponder, ) { let request = AssetRequest::from(request); // 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 tag and insert our little module loader there. - let body = match custom_index { - Some(custom_index) => custom_index - .replace( - "", - &format!("{}", module_loader(root_name, headless)), - ) - .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); - } - - template - .replace( - "", - &module_loader(root_name, headless), - ) - .into_bytes() - } - }; - - match Response::builder() - .header("Content-Type", "text/html") - .header("Access-Control-Allow-Origin", "*") - .body(Cow::from(body)) - { - Ok(response) => { - responder.respond(response); - return; - } - Err(err) => tracing::error!("error building response: {}", err), + match build_index_file(custom_index, custom_head, root_name, headless) { + Ok(response) => return responder.respond(response), + Err(err) => return tracing::error!("error building response: {}", err), } - } else if request.uri().path().trim_matches('/') == "edits" { - edit_queue.handle_request(responder); - return; } - // If the user provided a custom asset handler, then call it and return the response - // if the request was handled. + // 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. if let Some(response) = asset_handlers.try_handlers(&request).await { - responder.respond(response); - return; + return responder.respond(response); } // Else, try to serve a file from the filesystem. + match serve_from_fs(request) { + Ok(res) => responder.respond(res), + Err(e) => tracing::error!("Error serving request from filesystem {}", e), + } +} +fn serve_from_fs(request: AssetRequest) -> Result { // 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(&request.path); + // If we can't find it, make it absolute and try again if !asset.exists() { asset = PathBuf::from("/").join(request.path); } if asset.exists() { - let content_type = match get_mime_from_path(&asset) { - Ok(content_type) => content_type, - Err(err) => { - tracing::error!("error getting mime type: {}", err); - return; - } - }; - let asset = match std::fs::read(asset) { - Ok(asset) => asset, - Err(err) => { - tracing::error!("error reading asset: {}", err); - return; - } - }; - match Response::builder() - .header("Content-Type", content_type) - .body(Cow::from(asset)) - { - Ok(response) => { - responder.respond(response); - return; - } - Err(err) => tracing::error!("error building response: {}", err), - } - } + let content_type = get_mime_from_path(&asset)?; + let asset = std::fs::read(asset)?; - match Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Cow::from(String::from("Not Found").into_bytes())) - { - Ok(response) => { - responder.respond(response); - return; - } - Err(err) => tracing::error!("error building response: {}", err), + Ok(Response::builder() + .header("Content-Type", content_type) + .body(Cow::from(asset))?) + } else { + Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Cow::from(String::from("Not Found").into_bytes()))?) } } -fn module_loader(root_name: &str, headless: bool) -> String { +/// 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. +fn build_index_file( + custom_index: Option, + custom_head: Option, + root_name: &str, + headless: bool, +) -> std::result::Result>, wry::http::Error> { + // 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("").expect("Head element to exist"), &head); + } + + // Inject our module loader + index.insert_str( + index.find("").expect("Body element to exist"), + &module_loader(root_name, headless), + ); + + Response::builder() + .header("Content-Type", "text/html") + .header("Access-Control-Allow-Origin", "*") + .body(Cow::from(index.into_bytes())) +} + +/// 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#"