blitz_shell/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3//! Event loop, windowing and system integration.
4//!
5//! ## Feature flags
6//!  - `default`: Enables the features listed below.
7//!  - `accessibility`: Enables [`accesskit`] accessibility support.
8//!  - `hot-reload`: Enables hot-reloading of Dioxus RSX.
9//!  - `tracing`: Enables tracing support.
10
11mod application;
12mod convert_events;
13mod event;
14mod window;
15
16#[cfg(feature = "accessibility")]
17mod accessibility;
18
19pub use crate::application::BlitzApplication;
20pub use crate::event::BlitzShellEvent;
21pub use crate::window::{View, WindowConfig};
22
23use blitz_dom::net::Resource;
24use blitz_traits::net::NetCallback;
25#[cfg(all(
26    feature = "file_dialog",
27    any(
28        target_os = "windows",
29        target_os = "macos",
30        target_os = "linux",
31        target_os = "dragonfly",
32        target_os = "freebsd",
33        target_os = "netbsd",
34        target_os = "openbsd"
35    )
36))]
37use blitz_traits::shell::FileDialogFilter;
38use blitz_traits::shell::ShellProvider;
39use std::sync::Arc;
40pub use winit::event_loop::{ControlFlow, EventLoop, EventLoopProxy};
41pub use winit::window::{CursorIcon, Window};
42
43#[derive(Default)]
44pub struct Config {
45    pub stylesheets: Vec<String>,
46    pub base_url: Option<String>,
47}
48
49/// Build an event loop for the application
50pub fn create_default_event_loop<Event>() -> EventLoop<Event> {
51    let mut ev_builder = EventLoop::<Event>::with_user_event();
52    #[cfg(target_os = "android")]
53    {
54        use winit::platform::android::EventLoopBuilderExtAndroid;
55        ev_builder.with_android_app(current_android_app());
56    }
57
58    let event_loop = ev_builder.build().unwrap();
59    event_loop.set_control_flow(ControlFlow::Wait);
60
61    event_loop
62}
63
64#[cfg(target_os = "android")]
65static ANDROID_APP: std::sync::OnceLock<android_activity::AndroidApp> = std::sync::OnceLock::new();
66
67#[cfg(target_os = "android")]
68#[cfg_attr(docsrs, doc(cfg(target_os = "android")))]
69/// Set the current [`AndroidApp`](android_activity::AndroidApp).
70pub fn set_android_app(app: android_activity::AndroidApp) {
71    ANDROID_APP.set(app).unwrap()
72}
73
74#[cfg(target_os = "android")]
75#[cfg_attr(docsrs, doc(cfg(target_os = "android")))]
76/// Get the current [`AndroidApp`](android_activity::AndroidApp).
77/// This will panic if the android activity has not been setup with [`set_android_app`].
78pub fn current_android_app() -> android_activity::AndroidApp {
79    ANDROID_APP.get().unwrap().clone()
80}
81
82/// A NetCallback that injects the fetched Resource into our winit event loop
83pub struct BlitzShellNetCallback(EventLoopProxy<BlitzShellEvent>);
84
85impl BlitzShellNetCallback {
86    pub fn new(proxy: EventLoopProxy<BlitzShellEvent>) -> Self {
87        Self(proxy)
88    }
89
90    pub fn shared(proxy: EventLoopProxy<BlitzShellEvent>) -> Arc<dyn NetCallback<Resource>> {
91        Arc::new(Self(proxy))
92    }
93}
94impl NetCallback<Resource> for BlitzShellNetCallback {
95    fn call(&self, doc_id: usize, result: Result<Resource, Option<String>>) {
96        // TODO: handle error case
97        if let Ok(data) = result {
98            self.0
99                .send_event(BlitzShellEvent::ResourceLoad { doc_id, data })
100                .unwrap()
101        }
102    }
103}
104
105pub struct BlitzShellProvider {
106    window: Arc<Window>,
107}
108impl BlitzShellProvider {
109    pub fn new(window: Arc<Window>) -> Self {
110        Self { window }
111    }
112}
113
114impl ShellProvider for BlitzShellProvider {
115    fn request_redraw(&self) {
116        self.window.request_redraw();
117    }
118    fn set_cursor(&self, icon: CursorIcon) {
119        self.window.set_cursor(icon);
120    }
121    fn set_window_title(&self, title: String) {
122        self.window.set_title(&title);
123    }
124
125    #[cfg(all(
126        feature = "clipboard",
127        any(
128            target_os = "windows",
129            target_os = "macos",
130            target_os = "linux",
131            target_os = "dragonfly",
132            target_os = "freebsd",
133            target_os = "netbsd",
134            target_os = "openbsd"
135        )
136    ))]
137    fn get_clipboard_text(&self) -> Result<String, blitz_traits::shell::ClipboardError> {
138        let mut cb = arboard::Clipboard::new().unwrap();
139        cb.get_text()
140            .map_err(|_| blitz_traits::shell::ClipboardError)
141    }
142
143    #[cfg(all(
144        feature = "clipboard",
145        any(
146            target_os = "windows",
147            target_os = "macos",
148            target_os = "linux",
149            target_os = "dragonfly",
150            target_os = "freebsd",
151            target_os = "netbsd",
152            target_os = "openbsd"
153        )
154    ))]
155    fn set_clipboard_text(&self, text: String) -> Result<(), blitz_traits::shell::ClipboardError> {
156        let mut cb = arboard::Clipboard::new().unwrap();
157        cb.set_text(text.to_owned())
158            .map_err(|_| blitz_traits::shell::ClipboardError)
159    }
160
161    #[cfg(all(
162        feature = "file_dialog",
163        any(
164            target_os = "windows",
165            target_os = "macos",
166            target_os = "linux",
167            target_os = "dragonfly",
168            target_os = "freebsd",
169            target_os = "netbsd",
170            target_os = "openbsd"
171        )
172    ))]
173    fn open_file_dialog(
174        &self,
175        multiple: bool,
176        filter: Option<FileDialogFilter>,
177    ) -> Vec<std::path::PathBuf> {
178        let mut dialog = rfd::FileDialog::new();
179        if let Some(FileDialogFilter { name, extensions }) = filter {
180            dialog = dialog.add_filter(&name, &extensions);
181        }
182        let files = if multiple {
183            dialog.pick_files()
184        } else {
185            dialog.pick_file().map(|file| vec![file])
186        };
187        files.unwrap_or_default()
188    }
189}