Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-core/src/platform/app.rs
1use std::path::PathBuf;
2 
3use futures_util::future::LocalBoxFuture;
4 
5use crate::modals::ModalId;
6use crate::windowing::state::ApplicationStage;
7use crate::windowing::{WindowCallbackDispatcher, WindowManager};
8use crate::{
9 keymap::Keystroke, notification, AppContext, ClosedWindowData, SingletonEntity, WindowId,
10};
11 
12use super::menu::MenuItemPropertyChanges;
13 
14pub type AppInitCallbackFn =
15 Box<dyn FnOnce(&mut crate::AppContext, LocalBoxFuture<'static, crate::App>)>;
16 
17pub type TerminationResult = anyhow::Result<()>;
18 
19/// A collection of callbacks which application developers can provide
20/// to hook into important events that are observed by the UI framework.
21#[derive(Default)]
22#[allow(clippy::type_complexity)]
23pub struct AppCallbacks {
24 pub on_become_active: Option<Box<dyn FnMut(&mut AppContext)>>,
25 pub on_notification_clicked:
26 Option<Box<dyn FnMut(notification::NotificationResponse, &mut AppContext)>>,
27 pub on_resigned_active: Option<Box<dyn FnMut(&mut AppContext)>>,
28 pub on_will_terminate: Option<Box<dyn FnMut(&mut AppContext)>>,
29 /// Callback on whether the app will proceed with termination.
30 pub on_should_terminate_app: Option<Box<dyn FnMut(&mut AppContext) -> ApproveTerminateResult>>,
31 /// Callback on whether the window will proceed with closing.
32 pub on_should_close_window:
33 Option<Box<dyn FnMut(WindowId, &mut AppContext) -> ApproveTerminateResult>>,
34 /// Callback for when the user clicks "don't show again" on the warning modal.
35 pub on_disable_warning_modal: Option<Box<dyn FnMut(&mut AppContext)>>,
36 /// Callback on when the internet reachability to a specific host has changed.
37 /// The host name here could be a string for an IP address or domain (e.g. www.warp.dev).
38 pub on_internet_reachability_changed: Option<Box<dyn FnMut(bool, &mut AppContext)>>,
39 pub on_active_window_changed: Option<Box<dyn FnMut(&mut AppContext)>>,
40 pub on_new_window_requested: Option<Box<dyn FnMut(&mut AppContext)>>,
41 pub on_window_moved: Option<Box<dyn FnMut(&mut AppContext)>>,
42 pub on_window_resized: Option<Box<dyn FnMut(&mut AppContext)>>,
43 pub on_window_will_close: Option<Box<dyn FnMut(Option<ClosedWindowData>, &mut AppContext)>>,
44 /// Callback for screen parameter changes. For example, user connecting/
45 /// disconnecting external monitor, changes screen arrangement, or adjusts screen
46 /// resolution.
47 pub on_screen_changed: Option<Box<dyn FnMut(&mut AppContext)>>,
48 pub on_open_files: Option<Box<dyn FnMut(Vec<PathBuf>, &mut AppContext)>>,
49 pub on_open_urls: Option<Box<dyn FnMut(Vec<String>, &mut AppContext)>>,
50 pub on_os_appearance_changed: Option<Box<dyn FnMut(&mut AppContext)>>,
51 /// Callback to hook into a notification for when the cpu was awakened after sleeping.
52 pub on_cpu_awakened: Option<Box<dyn FnMut(&mut AppContext)>>,
53 /// Callback to hook into a notification for when the cpu is about to go to sleep.
54 pub on_cpu_will_sleep: Option<Box<dyn FnMut(&mut AppContext)>>,
55}
56 
57/// A helper structure to simplify and standardize the act of making calls from
58/// platform code into user application code.
59pub struct AppCallbackDispatcher {
60 callbacks: AppCallbacks,
61 ui_app: crate::App,
62}
63 
64pub enum ApproveTerminateResult {
65 /// The window or app should be closed.
66 Terminate,
67 /// Do not close the window or app.
68 Cancel,
69}
70 
71impl AppCallbackDispatcher {
72 pub fn new(callbacks: AppCallbacks, ui_app: crate::App) -> Self {
73 Self { callbacks, ui_app }
74 }
75 
76 pub fn initialize_app(&mut self, init_fn: AppInitCallbackFn) {
77 let app_clone = self.ui_app.clone();
78 self.ui_app.update(|ctx| {
79 use futures_util::FutureExt;
80 
81 // Provide the init function with access to the UI app,
82 // but only from a future that is running on the main
83 // thread (to prevent double-borrow issues).
84 init_fn(ctx, async move { app_clone }.boxed_local());
85 
86 // Validate all of the registered bindings now that the app is initialized.
87 ctx.validate_bindings();
88 });
89 }
90 
91 pub fn app_became_active(&mut self) {
92 log::info!("application did become active");
93 self.ui_app.update(|ctx| {
94 WindowManager::handle(ctx).update(ctx, |windowing_state, ctx| {
95 windowing_state.set_stage(ApplicationStage::Active, ctx);
96 });
97 });
98 
99 if let Some(callback) = &mut self.callbacks.on_become_active {
100 self.ui_app.update(|ctx| callback(ctx));
101 }
102 }
103 
104 // This is not called on Linux or wasm, as there isn't any generic way to
105 // click on/interact with a notification.
106 // TODO(CORE-2322): implement desktop notifications on Windows
107 #[cfg_attr(
108 any(target_os = "linux", target_os = "windows", target_family = "wasm"),
109 allow(dead_code)
110 )]
111 pub fn notification_clicked(&mut self, response: notification::NotificationResponse) {
112 if let Some(callback) = &mut self.callbacks.on_notification_clicked {
113 self.ui_app.update(|ctx| callback(response, ctx));
114 }
115 }
116 
117 pub fn app_resigned_active(&mut self) {
118 self.ui_app.update(|ctx| {
119 WindowManager::handle(ctx).update(ctx, |state, ctx| {
120 state.set_stage(ApplicationStage::Inactive, ctx);
121 });
122 });
123 if let Some(callback) = &mut self.callbacks.on_resigned_active {
124 self.ui_app.update(|ctx| callback(ctx));
125 }
126 }
127 
128 pub fn app_will_terminate(&mut self) {
129 log::info!("application will terminate");
130 self.ui_app.update(|ctx| {
131 WindowManager::handle(ctx).update(ctx, |state, ctx| {
132 state.set_stage(ApplicationStage::Terminating, ctx);
133 });
134 });
135 
136 if let Some(callback) = &mut self.callbacks.on_will_terminate {
137 self.ui_app.update(|ctx| callback(ctx));
138 }
139 }
140 
141 pub fn should_terminate_app(&mut self) -> ApproveTerminateResult {
142 if let Some(callback) = &mut self.callbacks.on_should_terminate_app {
143 self.ui_app.update(|ctx| callback(ctx))
144 } else {
145 ApproveTerminateResult::Terminate
146 }
147 }
148 
149 pub fn should_close_window(&mut self, window_id: WindowId) -> ApproveTerminateResult {
150 if let Some(callback) = &mut self.callbacks.on_should_close_window {
151 self.ui_app.update(|ctx| callback(window_id, ctx))
152 } else {
153 ApproveTerminateResult::Terminate
154 }
155 }
156 
157 // Dead code is allowed on wasm as when we register the network connection
158 // listener on wasm, we don't yet have access to an `AppCallbackDispatcher`,
159 // so we directly check the `Callbacks` object instead.
160 // TODO(CORE-2683): implement events for internet reachability changes
161 #[cfg_attr(any(target_family = "wasm", target_os = "windows"), allow(dead_code))]
162 pub fn has_internet_reachability_changed_callback(&self) -> bool {
163 self.callbacks.on_internet_reachability_changed.is_some()
164 }
165 
166 pub fn internet_reachability_changed(&mut self, is_reachable: bool) {
167 if is_reachable {
168 log::info!("application can reach internet");
169 } else {
170 log::info!("application can not reach internet");
171 }
172 if let Some(callback) = &mut self.callbacks.on_internet_reachability_changed {
173 self.ui_app.update(|ctx| callback(is_reachable, ctx));
174 }
175 }
176 
177 pub fn active_window_changed(&mut self, active_window_id: Option<WindowId>) {
178 log::info!("active window changed: {active_window_id:?}");
179 self.ui_app.update(|ctx| {
180 WindowManager::handle(ctx).update(ctx, |state, ctx| {
181 state.set_active_window(active_window_id, ctx);
182 });
183 });
184 
185 if let Some(callback) = &mut self.callbacks.on_active_window_changed {
186 self.ui_app.update(|ctx| callback(ctx));
187 }
188 }
189 
190 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
191 pub fn open_new_window(&mut self) {
192 if let Some(callback) = &mut self.callbacks.on_new_window_requested {
193 self.ui_app.update(|ctx| callback(ctx));
194 }
195 }
196 
197 pub fn window_moved(&mut self) {
198 if let Some(callback) = &mut self.callbacks.on_window_moved {
199 self.ui_app.update(|ctx| callback(ctx));
200 }
201 }
202 
203 pub fn window_resized(&mut self) {
204 log::info!("window resized");
205 if let Some(callback) = &mut self.callbacks.on_window_resized {
206 self.ui_app.update(|ctx| callback(ctx));
207 }
208 }
209 
210 pub fn window_will_close(&mut self, window_id: WindowId) {
211 log::info!("{window_id:?} will close");
212 if let Some(callback) = &mut self.callbacks.on_window_will_close {
213 self.ui_app.update(|ctx| {
214 let closed_window_data = ctx.handle_window_closed(window_id);
215 callback(closed_window_data, ctx);
216 });
217 }
218 }
219 
220 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
221 pub fn screen_changed(&mut self) {
222 if let Some(callback) = &mut self.callbacks.on_screen_changed {
223 self.ui_app.update(|ctx| callback(ctx));
224 }
225 
226 // Update the window fullscreen state, which in turn triggers a re-render of the workspace view.
227 self.ui_app.update(|ctx| {
228 WindowManager::handle(ctx).update(ctx, |state, model_ctx| {
229 state.update_is_active_window_fullscreen(model_ctx);
230 });
231 });
232 }
233 
234 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
235 pub fn open_files(&mut self, file_paths: Vec<PathBuf>) {
236 if let Some(callback) = &mut self.callbacks.on_open_files {
237 self.ui_app.update(|ctx| callback(file_paths, ctx));
238 }
239 }
240 
241 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
242 pub fn open_urls(&mut self, urls: Vec<String>) {
243 if let Some(callback) = &mut self.callbacks.on_open_urls {
244 self.ui_app.update(|ctx| callback(urls, ctx));
245 }
246 }
247 
248 pub fn os_appearance_changed(&mut self) {
249 if let Some(callback) = &mut self.callbacks.on_os_appearance_changed {
250 self.ui_app.update(|ctx| callback(ctx));
251 }
252 }
253 
254 pub fn cpu_awakened(&mut self) {
255 if let Some(callback) = &mut self.callbacks.on_cpu_awakened {
256 self.ui_app.update(|ctx| callback(ctx));
257 }
258 }
259 
260 pub fn cpu_will_sleep(&mut self) {
261 if let Some(callback) = &mut self.callbacks.on_cpu_will_sleep {
262 self.ui_app.update(|ctx| callback(ctx));
263 }
264 }
265 
266 pub fn global_shortcut_triggered(&mut self, shortcut: Keystroke) {
267 self.ui_app
268 .update(|ctx| ctx.on_global_shortcut_triggered(shortcut))
269 }
270 
271 #[cfg_attr(not(target_os = "macos"), allow(unused))]
272 pub fn can_borrow_mut(&self) -> bool {
273 self.ui_app.can_borrow_mut()
274 }
275 
276 pub fn with_mutable_app_context<T>(
277 &mut self,
278 callback: impl FnOnce(&mut AppContext) -> T,
279 ) -> T {
280 self.ui_app.update(|ctx| callback(ctx))
281 }
282 
283 pub fn for_window<'a>(
284 &'a mut self,
285 window: &'a dyn super::Window,
286 ) -> WindowCallbackDispatcher<'a> {
287 WindowCallbackDispatcher::new(window.callbacks(), self.ui_app.as_mut())
288 }
289}
290 
291// Functions in AppCallbackDispatcher that relate to application menus.
292//
293// This is marked as `allow(dead_code)` on Linux, as it doesn't support
294// application menus, so these never get called.
295// TODO(CORE-2691): implement native Windows OS app menus
296#[cfg_attr(
297 any(target_os = "linux", target_os = "windows", target_family = "wasm"),
298 allow(dead_code)
299)]
300impl AppCallbackDispatcher {
301 pub fn menu_item_triggered(&mut self, callback: impl FnOnce(&mut AppContext)) {
302 self.ui_app.update(callback);
303 }
304 
305 pub fn update_menu_item(
306 &mut self,
307 callback: impl FnOnce(&mut AppContext) -> MenuItemPropertyChanges,
308 ) -> MenuItemPropertyChanges {
309 self.ui_app.update(callback)
310 }
311}
312 
313// Functions in AppCallbackDispatcher that relate to native platform modals.
314//
315// This is marked as `allow(dead_code)` on Linux and WASM, as we do not support
316// native platform modals on these platforms, so these never get called.
317// TODO(CORE-2323): implement native Windows OS modal
318#[cfg_attr(
319 any(target_os = "linux", target_os = "windows", target_family = "wasm"),
320 allow(dead_code)
321)]
322impl AppCallbackDispatcher {
323 pub fn process_platform_modal_response(
324 &mut self,
325 modal_id: ModalId,
326 response_button_index: usize,
327 disable_modal: bool,
328 ) {
329 self.ui_app.update(|ctx| {
330 ctx.process_platform_modal_response(modal_id, response_button_index, disable_modal)
331 });
332 }
333 
334 pub fn warning_modal_disabled(&mut self) {
335 if let Some(callback) = &mut self.callbacks.on_disable_warning_modal {
336 self.ui_app.update(|ctx| callback(ctx));
337 }
338 }
339}
340