lib.rs 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. //! Dioxus Desktop Renderer
  2. //!
  3. //! Render the Dioxus VirtualDom using the platform's native WebView implementation.
  4. //!
  5. use std::borrow::BorrowMut;
  6. use std::cell::{Cell, RefCell};
  7. use std::collections::{HashMap, VecDeque};
  8. use std::ops::{Deref, DerefMut};
  9. use std::rc::Rc;
  10. use std::sync::atomic::AtomicBool;
  11. use std::sync::mpsc::channel;
  12. use std::sync::{Arc, RwLock};
  13. use cfg::DesktopConfig;
  14. use dioxus_core::*;
  15. use serde::{Deserialize, Serialize};
  16. pub use wry;
  17. use wry::application::accelerator::{Accelerator, SysMods};
  18. use wry::application::event::{ElementState, Event, StartCause, WindowEvent};
  19. use wry::application::event_loop::{self, ControlFlow, EventLoop, EventLoopWindowTarget};
  20. use wry::application::keyboard::{Key, KeyCode, ModifiersState};
  21. use wry::application::menu::{MenuBar, MenuItem, MenuItemAttributes};
  22. use wry::application::window::{Fullscreen, WindowId};
  23. use wry::webview::{WebView, WebViewBuilder};
  24. use wry::{
  25. application::menu,
  26. application::window::{Window, WindowBuilder},
  27. webview::{RpcRequest, RpcResponse},
  28. };
  29. mod cfg;
  30. mod desktop_context;
  31. mod dom;
  32. mod escape;
  33. mod events;
  34. static HTML_CONTENT: &'static str = include_str!("./index.html");
  35. pub fn launch(
  36. root: FC<()>,
  37. config_builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>,
  38. ) {
  39. launch_with_props(root, (), config_builder)
  40. }
  41. pub fn launch_with_props<P: Properties + 'static + Send + Sync>(
  42. root: FC<P>,
  43. props: P,
  44. builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>,
  45. ) {
  46. run(root, props, builder)
  47. }
  48. #[derive(Serialize)]
  49. enum RpcEvent<'a> {
  50. Initialize { edits: Vec<DomEdit<'a>> },
  51. }
  52. #[derive(Serialize)]
  53. struct Response<'a> {
  54. pre_rendered: Option<String>,
  55. edits: Vec<DomEdit<'a>>,
  56. }
  57. pub fn run<T: 'static + Send + Sync>(
  58. root: FC<T>,
  59. props: T,
  60. user_builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>,
  61. ) {
  62. // Generate the config
  63. let mut cfg = DesktopConfig::new();
  64. user_builder(&mut cfg);
  65. let DesktopConfig {
  66. window,
  67. manual_edits,
  68. pre_rendered,
  69. ..
  70. } = cfg;
  71. // All of our webview windows are stored in a way that we can look them up later
  72. // The "DesktopContext" will provide functionality for spawning these windows
  73. let mut webviews = HashMap::<WindowId, WebView>::new();
  74. let event_loop = EventLoop::new();
  75. let props_shared = Cell::new(Some(props));
  76. // create local modifier state
  77. let modifiers = ModifiersState::default();
  78. let quit_hotkey = Accelerator::new(SysMods::Cmd, KeyCode::KeyQ);
  79. let edit_queue = Arc::new(RwLock::new(VecDeque::new()));
  80. let is_ready: Arc<AtomicBool> = Default::default();
  81. event_loop.run(move |window_event, event_loop, control_flow| {
  82. *control_flow = ControlFlow::Wait;
  83. match window_event {
  84. Event::NewEvents(StartCause::Init) => {
  85. let window = create_window(event_loop);
  86. let window_id = window.id();
  87. let sender =
  88. launch_vdom_with_tokio(root, props_shared.take().unwrap(), edit_queue.clone());
  89. let webview = create_webview(window, is_ready.clone(), sender);
  90. webviews.insert(window_id, webview);
  91. }
  92. Event::WindowEvent {
  93. event, window_id, ..
  94. } => match event {
  95. WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
  96. WindowEvent::Destroyed { .. } => {
  97. webviews.remove(&window_id);
  98. if webviews.is_empty() {
  99. *control_flow = ControlFlow::Exit;
  100. }
  101. }
  102. WindowEvent::Moved(pos) => {
  103. //
  104. }
  105. WindowEvent::KeyboardInput { event, .. } => {
  106. if quit_hotkey.matches(&modifiers, &event.physical_key) {
  107. webviews.remove(&window_id);
  108. if webviews.is_empty() {
  109. *control_flow = ControlFlow::Exit;
  110. }
  111. }
  112. }
  113. WindowEvent::Resized(_) | WindowEvent::Moved(_) => {
  114. if let Some(view) = webviews.get_mut(&window_id) {
  115. let _ = view.resize();
  116. }
  117. }
  118. // TODO: we want to shuttle all of these events into the user's app
  119. _ => {}
  120. },
  121. Event::MainEventsCleared => {
  122. // I hate this ready hack but it's needed to wait for the "onload" to occur
  123. // We can't run any initializion scripts because the window isn't ready yet?
  124. if is_ready.load(std::sync::atomic::Ordering::Relaxed) {
  125. let mut queue = edit_queue.write().unwrap();
  126. let (id, view) = webviews.iter_mut().next().unwrap();
  127. while let Some(edit) = queue.pop_back() {
  128. view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit))
  129. .unwrap();
  130. }
  131. }
  132. }
  133. Event::Resumed => {}
  134. Event::Suspended => {}
  135. Event::LoopDestroyed => {}
  136. _ => {}
  137. }
  138. })
  139. }
  140. // Create a new tokio runtime on a dedicated thread and then launch the apps VirtualDom.
  141. pub(crate) fn launch_vdom_with_tokio<P: Send + 'static>(
  142. root: FC<P>,
  143. props: P,
  144. edit_queue: Arc<RwLock<VecDeque<String>>>,
  145. ) -> futures_channel::mpsc::UnboundedSender<SchedulerMsg> {
  146. let (sender, receiver) = futures_channel::mpsc::unbounded::<SchedulerMsg>();
  147. let return_sender = sender.clone();
  148. std::thread::spawn(move || {
  149. // We create the runtim as multithreaded, so you can still "spawn" onto multiple threads
  150. let runtime = tokio::runtime::Builder::new_multi_thread()
  151. .enable_all()
  152. .build()
  153. .unwrap();
  154. runtime.block_on(async move {
  155. let mut dom = VirtualDom::new_with_props_and_scheduler(root, props, sender, receiver);
  156. let edits = dom.rebuild();
  157. edit_queue
  158. .write()
  159. .unwrap()
  160. .push_front(serde_json::to_string(&edits.edits).unwrap());
  161. loop {
  162. dom.wait_for_work().await;
  163. let mut muts = dom.work_with_deadline(|| false);
  164. while let Some(edit) = muts.pop() {
  165. edit_queue
  166. .write()
  167. .unwrap()
  168. .push_front(serde_json::to_string(&edit.edits).unwrap());
  169. }
  170. }
  171. })
  172. });
  173. return_sender
  174. }
  175. fn build_menu() -> MenuBar {
  176. // create main menubar menu
  177. let mut menu_bar_menu = MenuBar::new();
  178. // create `first_menu`
  179. let mut first_menu = MenuBar::new();
  180. first_menu.add_native_item(MenuItem::About("Todos".to_string()));
  181. first_menu.add_native_item(MenuItem::Services);
  182. first_menu.add_native_item(MenuItem::Separator);
  183. first_menu.add_native_item(MenuItem::Hide);
  184. first_menu.add_native_item(MenuItem::HideOthers);
  185. first_menu.add_native_item(MenuItem::ShowAll);
  186. first_menu.add_native_item(MenuItem::Quit);
  187. first_menu.add_native_item(MenuItem::CloseWindow);
  188. // create second menu
  189. let mut second_menu = MenuBar::new();
  190. // second_menu.add_submenu("Sub menu", true, my_sub_menu);
  191. second_menu.add_native_item(MenuItem::Copy);
  192. second_menu.add_native_item(MenuItem::Paste);
  193. second_menu.add_native_item(MenuItem::SelectAll);
  194. menu_bar_menu.add_submenu("First menu", true, first_menu);
  195. menu_bar_menu.add_submenu("Second menu", true, second_menu);
  196. menu_bar_menu
  197. }
  198. fn create_window(event_loop: &EventLoopWindowTarget<()>) -> Window {
  199. WindowBuilder::new()
  200. .with_maximized(true)
  201. .with_menu(build_menu())
  202. .with_title("Dioxus App")
  203. .build(event_loop)
  204. .unwrap()
  205. }
  206. fn create_webview(
  207. window: Window,
  208. is_ready: Arc<AtomicBool>,
  209. sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
  210. ) -> WebView {
  211. WebViewBuilder::new(window)
  212. .unwrap()
  213. .with_url("wry://index.html")
  214. .unwrap()
  215. .with_rpc_handler(move |_window: &Window, mut req: RpcRequest| {
  216. match req.method.as_str() {
  217. "user_event" => {
  218. let event = events::trigger_from_serialized(req.params.unwrap());
  219. log::debug!("User event: {:?}", event);
  220. sender.unbounded_send(SchedulerMsg::UiEvent(event)).unwrap();
  221. }
  222. "initialize" => {
  223. is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
  224. }
  225. _ => {}
  226. }
  227. None
  228. })
  229. // Any content that that uses the `wry://` scheme will be shuttled through this handler as a "special case"
  230. // For now, we only serve two pieces of content which get included as bytes into the final binary.
  231. .with_custom_protocol("wry".into(), move |request| {
  232. let path = request.uri().replace("wry://", "");
  233. let (data, meta) = match path.as_str() {
  234. "index.html" => (include_bytes!("./index.html").to_vec(), "text/html"),
  235. "index.html/index.js" => (include_bytes!("./index.js").to_vec(), "text/javascript"),
  236. _ => unimplemented!("path {}", path),
  237. };
  238. wry::http::ResponseBuilder::new().mimetype(meta).body(data)
  239. })
  240. .build()
  241. .unwrap()
  242. }