StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | use std::mem::ManuallyDrop; |
| 2 | use std::sync::mpsc::{Receiver, Sender}; |
| 3 | |
| 4 | use crate::{ |
| 5 | platform::{ |
| 6 | self, |
| 7 | app::{AppCallbackDispatcher, ApproveTerminateResult, TerminationResult}, |
| 8 | TerminationMode, |
| 9 | }, |
| 10 | AppContext, WindowId, |
| 11 | }; |
| 12 | |
| 13 | /// Application events handled on the headless platform's main thread. |
| 14 | pub(super) enum AppEvent { |
| 15 | /// Run the wrapped task on the main thread. |
| 16 | RunTask(ManuallyDrop<async_task::Runnable>), |
| 17 | /// Run a synchronous callback on the main thread. |
| 18 | RunCallback(Box<dyn FnOnce(&mut AppContext) + Send + Sync>), |
| 19 | /// Close a window. |
| 20 | CloseWindow(WindowId), |
| 21 | /// Active window changed. |
| 22 | ActiveWindowChanged(Option<WindowId>), |
| 23 | /// Exit the event loop, terminating the application. |
| 24 | Terminate(TerminationMode), |
| 25 | } |
| 26 | |
| 27 | /// Run a simple, blocking event loop that processes AppEvent messages until termination. |
| 28 | pub(super) fn run( |
| 29 | mut ui_app: crate::App, |
| 30 | callbacks: &mut AppCallbackDispatcher, |
| 31 | init_fn: platform::app::AppInitCallbackFn, |
| 32 | receiver: Receiver<AppEvent>, |
| 33 | sender: Sender<AppEvent>, |
| 34 | ) -> TerminationResult { |
| 35 | // Set up Ctrl-C handler to gracefully terminate the app |
| 36 | setup_signal_handler(sender); |
| 37 | |
| 38 | // First, initialize the app. |
| 39 | callbacks.initialize_app(init_fn); |
| 40 | |
| 41 | // Then, process events until termination. |
| 42 | for event in receiver.iter() { |
| 43 | match event { |
| 44 | AppEvent::RunCallback(callback) => ui_app.update(callback), |
| 45 | AppEvent::RunTask(task) => { |
| 46 | // Poll a task on the main thread. |
| 47 | let task = ManuallyDrop::into_inner(task); |
| 48 | task.run(); |
| 49 | } |
| 50 | AppEvent::Terminate(termination_mode) => { |
| 51 | let should_terminate = match termination_mode { |
| 52 | TerminationMode::Cancellable => { |
| 53 | matches!( |
| 54 | callbacks.should_terminate_app(), |
| 55 | ApproveTerminateResult::Terminate |
| 56 | ) |
| 57 | } |
| 58 | TerminationMode::ForceTerminate | TerminationMode::ContentTransferred => true, |
| 59 | }; |
| 60 | if should_terminate { |
| 61 | break; |
| 62 | } |
| 63 | } |
| 64 | AppEvent::CloseWindow(window_id) => { |
| 65 | // Notify the app that a window is closing. The app will then remove the window |
| 66 | // from WindowManager. |
| 67 | callbacks.window_will_close(window_id); |
| 68 | } |
| 69 | AppEvent::ActiveWindowChanged(window_id) => { |
| 70 | callbacks.active_window_changed(window_id); |
| 71 | } |
| 72 | } |
| 73 | } |
| 74 | |
| 75 | // Drop the receiver so the Ctrl+C signal handler's channel send will fail, |
| 76 | // causing it to fall through to `process::exit(130)`. Without this, the |
| 77 | // send succeeds (since the receiver is still in scope) but nobody is reading |
| 78 | // from the channel, making Ctrl+C ineffective during shutdown. |
| 79 | drop(receiver); |
| 80 | |
| 81 | callbacks.app_will_terminate(); |
| 82 | |
| 83 | ui_app.termination_result().unwrap_or(Ok(())) |
| 84 | } |
| 85 | |
| 86 | /// Set up a signal handler for Ctrl-C (SIGINT) to gracefully terminate the app. |
| 87 | /// |
| 88 | /// When Ctrl-C is received, this will send a Terminate event to the event loop, |
| 89 | /// allowing the app to shut down gracefully via the existing termination logic. |
| 90 | #[cfg(not(target_family = "wasm"))] |
| 91 | fn setup_signal_handler(sender: Sender<AppEvent>) { |
| 92 | let result = ctrlc::set_handler(move || { |
| 93 | log::info!("Received Ctrl-C signal in headless mode, terminating application"); |
| 94 | // Send a ForceTerminate event to ensure the app exits cleanly. |
| 95 | // We use ForceTerminate rather than Cancellable to ensure the app exits |
| 96 | // even if there are unsaved changes or other conditions that might prevent shutdown. |
| 97 | if sender |
| 98 | .send(AppEvent::Terminate(TerminationMode::ForceTerminate)) |
| 99 | .is_err() |
| 100 | { |
| 101 | log::warn!("Failed to send termination event - event loop may have already stopped"); |
| 102 | // If we can't send the event, force exit |
| 103 | std::process::exit(130); // 128 + SIGINT (2) = 130 |
| 104 | } |
| 105 | }); |
| 106 | |
| 107 | if let Err(e) = result { |
| 108 | log::warn!("Failed to set up Ctrl-C handler: {e}"); |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | #[cfg(target_family = "wasm")] |
| 113 | fn setup_signal_handler(_sender: Sender<AppEvent>) { |
| 114 | // No signal handling on WASM |
| 115 | } |
| 116 |