dioxus_desktop/
desktop_context.rs

1use crate::{
2    app::SharedContext,
3    assets::AssetHandlerRegistry,
4    file_upload::NativeFileHover,
5    ipc::UserWindowEvent,
6    query::QueryEngine,
7    shortcut::{HotKey, HotKeyState, ShortcutHandle, ShortcutRegistryError},
8    webview::PendingWebview,
9    AssetRequest, Config, WindowCloseBehaviour, WryEventHandler,
10};
11use dioxus_core::{Callback, VirtualDom};
12use std::{
13    cell::Cell,
14    future::{Future, IntoFuture},
15    pin::Pin,
16    rc::{Rc, Weak},
17    sync::Arc,
18};
19use tao::{
20    event::Event,
21    event_loop::EventLoopWindowTarget,
22    window::{Fullscreen as WryFullscreen, Window, WindowId},
23};
24use wry::{RequestAsyncResponder, WebView};
25
26#[cfg(target_os = "ios")]
27use tao::platform::ios::WindowExtIOS;
28
29/// Get an imperative handle to the current window without using a hook
30///
31/// ## Panics
32///
33/// This function will panic if it is called outside of the context of a Dioxus App.
34pub fn window() -> DesktopContext {
35    dioxus_core::consume_context()
36}
37
38/// A handle to the [`DesktopService`] that can be passed around.
39pub type DesktopContext = Rc<DesktopService>;
40
41/// A weak handle to the [`DesktopService`] to ensure safe passing.
42/// The problem without this is that the tao window is never dropped and therefore cannot be closed.
43/// This was due to the Rc that had still references because of multiple copies when creating a webview.
44pub type WeakDesktopContext = Weak<DesktopService>;
45
46/// An imperative interface to the current window.
47///
48/// To get a handle to the current window, use the [`window`] function.
49///
50///
51/// # Example
52///
53/// you can use `cx.consume_context::<DesktopContext>` to get this context
54///
55/// ```rust, ignore
56///     let desktop = cx.consume_context::<DesktopContext>().unwrap();
57/// ```
58pub struct DesktopService {
59    /// The wry/tao proxy to the current window
60    pub webview: WebView,
61
62    /// The tao window itself
63    pub window: Arc<Window>,
64
65    pub(crate) shared: Rc<SharedContext>,
66
67    /// The receiver for queries about the current window
68    pub(super) query: QueryEngine,
69    pub(crate) asset_handlers: AssetHandlerRegistry,
70    pub(crate) file_hover: NativeFileHover,
71    pub(crate) close_behaviour: Rc<Cell<WindowCloseBehaviour>>,
72
73    #[cfg(target_os = "ios")]
74    pub(crate) views: Rc<std::cell::RefCell<Vec<*mut objc::runtime::Object>>>,
75}
76
77/// A smart pointer to the current window.
78impl std::ops::Deref for DesktopService {
79    type Target = Window;
80
81    fn deref(&self) -> &Self::Target {
82        &self.window
83    }
84}
85
86impl DesktopService {
87    pub(crate) fn new(
88        webview: WebView,
89        window: Arc<Window>,
90        shared: Rc<SharedContext>,
91        asset_handlers: AssetHandlerRegistry,
92        file_hover: NativeFileHover,
93        close_behaviour: WindowCloseBehaviour,
94    ) -> Self {
95        Self {
96            window,
97            webview,
98            shared,
99            asset_handlers,
100            file_hover,
101            close_behaviour: Rc::new(Cell::new(close_behaviour)),
102            query: Default::default(),
103            #[cfg(target_os = "ios")]
104            views: Default::default(),
105        }
106    }
107
108    /// Start the creation of a new window using the props and window builder
109    ///
110    /// Returns a future that resolves to the webview handle for the new window. You can use this
111    /// to control other windows from the current window once the new window is created.
112    ///
113    /// Be careful to not create a cycle of windows, or you might leak memory.
114    ///
115    /// # Example
116    ///
117    /// ```rust, no_run
118    /// use dioxus::prelude::*;
119    /// fn popup() -> Element {
120    ///     rsx! {
121    ///         div { "This is a popup window!" }
122    ///     }
123    /// }
124    ///
125    /// # async fn app() {
126    /// // Create a new window with a component that will be rendered in the new window.
127    /// let dom = VirtualDom::new(popup);
128    /// // Create and wait for the window
129    /// let window = dioxus::desktop::window().new_window(dom, Default::default()).await;
130    /// // Fullscreen the new window
131    /// window.set_fullscreen(true);
132    /// # }
133    /// ```
134    // Note: This method is asynchronous because webview2 does not support creating a new window from
135    // inside of an existing webview callback. Dioxus runs event handlers synchronously inside of a webview
136    // callback. See [this page](https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/threading-model#reentrancy) for more information.
137    //
138    // Related issues:
139    // - https://github.com/tauri-apps/wry/issues/583
140    // - https://github.com/DioxusLabs/dioxus/issues/3080
141    pub fn new_window(&self, dom: VirtualDom, cfg: Config) -> PendingDesktopContext {
142        let (window, context) = PendingWebview::new(dom, cfg);
143
144        self.shared
145            .proxy
146            .send_event(UserWindowEvent::NewWindow)
147            .unwrap();
148
149        self.shared.pending_webviews.borrow_mut().push(window);
150
151        context
152    }
153
154    /// trigger the drag-window event
155    ///
156    /// Moves the window with the left mouse button until the button is released.
157    ///
158    /// you need use it in `onmousedown` event:
159    /// ```rust, ignore
160    /// onmousedown: move |_| { desktop.drag_window(); }
161    /// ```
162    pub fn drag(&self) {
163        if self.window.fullscreen().is_none() {
164            _ = self.window.drag_window();
165        }
166    }
167
168    /// Toggle whether the window is maximized or not
169    pub fn toggle_maximized(&self) {
170        self.window.set_maximized(!self.window.is_maximized())
171    }
172
173    /// Set the close behavior of this window
174    ///
175    /// By default, windows close when the user clicks the close button.
176    /// If this is set to `WindowCloseBehaviour::WindowHides`, the window will hide instead of closing.
177    pub fn set_close_behavior(&self, behaviour: WindowCloseBehaviour) {
178        self.close_behaviour.set(behaviour);
179    }
180
181    /// Close this window
182    pub fn close(&self) {
183        let _ = self
184            .shared
185            .proxy
186            .send_event(UserWindowEvent::CloseWindow(self.id()));
187    }
188
189    /// Close a particular window, given its ID
190    pub fn close_window(&self, id: WindowId) {
191        let _ = self
192            .shared
193            .proxy
194            .send_event(UserWindowEvent::CloseWindow(id));
195    }
196
197    /// change window to fullscreen
198    pub fn set_fullscreen(&self, fullscreen: bool) {
199        if let Some(handle) = &self.window.current_monitor() {
200            self.window.set_fullscreen(
201                fullscreen.then_some(WryFullscreen::Borderless(Some(handle.clone()))),
202            );
203        }
204    }
205
206    /// launch print modal
207    pub fn print(&self) {
208        if let Err(e) = self.webview.print() {
209            tracing::warn!("Open print modal failed: {e}");
210        }
211    }
212
213    /// Set the zoom level of the webview
214    pub fn set_zoom_level(&self, level: f64) {
215        if let Err(e) = self.webview.zoom(level) {
216            tracing::warn!("Set webview zoom failed: {e}");
217        }
218    }
219
220    /// opens DevTool window
221    pub fn devtool(&self) {
222        #[cfg(debug_assertions)]
223        self.webview.open_devtools();
224
225        #[cfg(not(debug_assertions))]
226        tracing::warn!("Devtools are disabled in release builds");
227    }
228
229    /// Create a wry event handler that listens for wry events.
230    /// This event handler is scoped to the currently active window and will only receive events that are either global or related to the current window.
231    ///
232    /// The id this function returns can be used to remove the event handler with [`Self::remove_wry_event_handler`]
233    pub fn create_wry_event_handler(
234        &self,
235        handler: impl FnMut(&Event<UserWindowEvent>, &EventLoopWindowTarget<UserWindowEvent>) + 'static,
236    ) -> WryEventHandler {
237        self.shared.event_handlers.add(self.window.id(), handler)
238    }
239
240    /// Remove a wry event handler created with [`Self::create_wry_event_handler`]
241    pub fn remove_wry_event_handler(&self, id: WryEventHandler) {
242        self.shared.event_handlers.remove(id)
243    }
244
245    /// Create a global shortcut
246    ///
247    /// Linux: Only works on x11. See [this issue](https://github.com/tauri-apps/tao/issues/331) for more information.
248    pub fn create_shortcut(
249        &self,
250        hotkey: HotKey,
251        callback: impl FnMut(HotKeyState) + 'static,
252    ) -> Result<ShortcutHandle, ShortcutRegistryError> {
253        self.shared
254            .shortcut_manager
255            .add_shortcut(hotkey, Box::new(callback))
256    }
257
258    /// Remove a global shortcut
259    pub fn remove_shortcut(&self, id: ShortcutHandle) {
260        self.shared.shortcut_manager.remove_shortcut(id)
261    }
262
263    /// Remove all global shortcuts
264    pub fn remove_all_shortcuts(&self) {
265        self.shared.shortcut_manager.remove_all()
266    }
267
268    /// Provide a callback to handle asset loading yourself.
269    /// If the ScopeId isn't provided, defaults to a global handler.
270    /// Note that the handler is namespaced by name, not ScopeId.
271    ///
272    /// When the component is dropped, the handler is removed.
273    ///
274    /// See [`crate::use_asset_handler`] for a convenient hook.
275    pub fn register_asset_handler(
276        &self,
277        name: String,
278        handler: impl Fn(AssetRequest, RequestAsyncResponder) + 'static,
279    ) {
280        self.asset_handlers
281            .register_handler(name, Callback::new(move |(req, resp)| handler(req, resp)))
282    }
283
284    /// Removes an asset handler by its identifier.
285    ///
286    /// Returns `None` if the handler did not exist.
287    pub fn remove_asset_handler(&self, name: &str) -> Option<()> {
288        self.asset_handlers.remove_handler(name).map(|_| ())
289    }
290
291    /// Push an objc view to the window
292    #[cfg(target_os = "ios")]
293    pub fn push_view(&self, view: objc_id::ShareId<objc::runtime::Object>) {
294        let window = &self.window;
295
296        unsafe {
297            use objc::runtime::Object;
298            use objc::*;
299            assert!(is_main_thread());
300            let ui_view = window.ui_view() as *mut Object;
301            let ui_view_frame: *mut Object = msg_send![ui_view, frame];
302            let _: () = msg_send![view, setFrame: ui_view_frame];
303            let _: () = msg_send![view, setAutoresizingMask: 31];
304
305            let ui_view_controller = window.ui_view_controller() as *mut Object;
306            let _: () = msg_send![ui_view_controller, setView: view];
307            self.views.borrow_mut().push(ui_view);
308        }
309    }
310
311    /// Pop an objc view from the window
312    #[cfg(target_os = "ios")]
313    pub fn pop_view(&self) {
314        let window = &self.window;
315
316        unsafe {
317            use objc::runtime::Object;
318            use objc::*;
319            assert!(is_main_thread());
320            if let Some(view) = self.views.borrow_mut().pop() {
321                let ui_view_controller = window.ui_view_controller() as *mut Object;
322                let _: () = msg_send![ui_view_controller, setView: view];
323            }
324        }
325    }
326}
327
328#[cfg(target_os = "ios")]
329fn is_main_thread() -> bool {
330    use objc::runtime::{Class, BOOL, NO};
331    use objc::*;
332
333    let cls = Class::get("NSThread").unwrap();
334    let result: BOOL = unsafe { msg_send![cls, isMainThread] };
335    result != NO
336}
337
338/// A [`DesktopContext`] that is pending creation.
339///
340/// # Example
341/// ```rust, no_run
342/// # use dioxus::prelude::*;
343/// # async fn app() {
344/// // Create a new window with a component that will be rendered in the new window.
345/// let dom = VirtualDom::new(|| rsx!{ "popup!" });
346///
347/// // Create a new window asynchronously
348/// let pending_context = dioxus::desktop::window().new_window(dom, Default::default());
349///
350/// // Wait for the context to be created
351/// let window = pending_context.await;
352///
353/// // Now control the window
354/// window.set_fullscreen(true);
355/// # }
356/// ```
357pub struct PendingDesktopContext {
358    pub(crate) receiver: futures_channel::oneshot::Receiver<DesktopContext>,
359}
360
361impl PendingDesktopContext {
362    /// Resolve the pending context into a [`DesktopContext`].
363    pub async fn resolve(self) -> DesktopContext {
364        self.try_resolve()
365            .await
366            .expect("Failed to resolve pending desktop context")
367    }
368
369    /// Try to resolve the pending context into a [`DesktopContext`].
370    pub async fn try_resolve(self) -> Result<DesktopContext, futures_channel::oneshot::Canceled> {
371        self.receiver.await
372    }
373}
374
375impl IntoFuture for PendingDesktopContext {
376    type Output = DesktopContext;
377
378    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output>>>;
379
380    fn into_future(self) -> Self::IntoFuture {
381        Box::pin(self.resolve())
382    }
383}