StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | use std::path::PathBuf; |
| 2 | |
| 3 | use futures_util::future::LocalBoxFuture; |
| 4 | |
| 5 | use crate::modals::ModalId; |
| 6 | use crate::windowing::state::ApplicationStage; |
| 7 | use crate::windowing::{WindowCallbackDispatcher, WindowManager}; |
| 8 | use crate::{ |
| 9 | keymap::Keystroke, notification, AppContext, ClosedWindowData, SingletonEntity, WindowId, |
| 10 | }; |
| 11 | |
| 12 | use super::menu::MenuItemPropertyChanges; |
| 13 | |
| 14 | pub type AppInitCallbackFn = |
| 15 | Box<dyn FnOnce(&mut crate::AppContext, LocalBoxFuture<'static, crate::App>)>; |
| 16 | |
| 17 | pub 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)] |
| 23 | pub 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. |
| 59 | pub struct AppCallbackDispatcher { |
| 60 | callbacks: AppCallbacks, |
| 61 | ui_app: crate::App, |
| 62 | } |
| 63 | |
| 64 | pub enum ApproveTerminateResult { |
| 65 | /// The window or app should be closed. |
| 66 | Terminate, |
| 67 | /// Do not close the window or app. |
| 68 | Cancel, |
| 69 | } |
| 70 | |
| 71 | impl 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 | )] |
| 300 | impl 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 | )] |
| 322 | impl 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 |