use crate::core::ObjectId;
use crate::WidgetTriggerEvent;
use super::handle::{dispatch_trigger, WindowHandle};
use super::lifecycle::AppLifecycle;
#[derive(Debug, Clone)]
pub struct AppConfig {
pub app_name: String,
pub organization: String,
pub version: String,
pub enable_i18n: bool,
pub enable_accessibility: bool,
}
impl Default for AppConfig {
fn default() -> Self {
Self {
app_name: String::new(),
organization: String::new(),
version: String::new(),
enable_i18n: true,
enable_accessibility: true,
}
}
}
impl AppConfig {
pub fn with_app_name(mut self, name: &str) -> Self {
self.app_name = name.to_owned();
self
}
pub fn with_organization(mut self, org: &str) -> Self {
self.organization = org.to_owned();
self
}
pub fn with_version(mut self, version: &str) -> Self {
self.version = version.to_owned();
self
}
pub fn with_i18n(mut self, enable: bool) -> Self {
self.enable_i18n = enable;
self
}
pub fn with_accessibility(mut self, enable: bool) -> Self {
self.enable_accessibility = enable;
self
}
}
pub struct App {
config: AppConfig,
lifecycle: AppLifecycle,
}
impl App {
pub fn new() -> Self {
Self { config: AppConfig::default(), lifecycle: AppLifecycle::new() }
}
pub fn with_config(config: AppConfig) -> Self {
Self { config, lifecycle: AppLifecycle::new() }
}
pub fn config(&self) -> &AppConfig {
&self.config
}
pub fn config_mut(&mut self) -> &mut AppConfig {
&mut self.config
}
pub fn lifecycle(&self) -> &AppLifecycle {
&self.lifecycle
}
pub fn lifecycle_mut(&mut self) -> &mut AppLifecycle {
&mut self.lifecycle
}
pub fn on_startup<F: FnOnce() + Send + 'static>(self, f: F) -> Self {
with_startup(|slot| {
*slot = Some(Box::new(f));
});
self
}
pub fn on_shutdown<F: FnOnce() + Send + 'static>(self, f: F) -> Self {
with_shutdown(|slot| {
*slot = Some(Box::new(f));
});
self
}
pub fn init(&mut self) {
trace_runtime_route("app::init");
crate::platform::profile::runtime_init();
if self.config.enable_i18n {
crate::platform::profile::init_optional_subsystems();
} else {
log::debug!("[app] i18n initialisation skipped: AppConfig::with_i18n(false) was set");
}
set_accessibility_enabled(self.config.enable_accessibility);
self.lifecycle.transition(crate::app::lifecycle::AppLifecycleState::Foreground);
fire_startup();
}
pub fn accessibility_enabled(&self) -> bool {
accessibility_enabled()
}
pub fn run(&self) {
trace_runtime_route("app::run");
crate::run();
fire_shutdown();
}
pub fn quit(&mut self) {
crate::quit();
self.lifecycle.transition(crate::app::lifecycle::AppLifecycleState::Terminating);
}
pub fn run_async(&self) -> std::thread::JoinHandle<()> {
trace_runtime_route("app::run_async");
std::thread::spawn(|| {
crate::run();
fire_shutdown();
})
}
pub fn new_window(&self, title: &str, x: i32, y: i32, w: u32, h: u32) -> WindowHandle {
let id = crate::create_window(title, x, y, w, h);
WindowHandle::record_created_geometry(id, x, y, w, h);
WindowHandle::from_raw(id)
}
pub fn poll_event(&self) -> Option<WidgetTriggerEvent> {
let ev = crate::poll_widget_trigger_event();
if let Some(ref ev) = ev {
dispatch_trigger(ev.widget_id, ev.kind);
}
ev
}
pub fn poll_triggered(&self) -> Option<ObjectId> {
crate::poll_widget_triggered()
}
}
crate::impl_default_via_new!(App);
use crate::compat::Mutex;
use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
use std::sync::OnceLock;
static STARTUP: OnceLock<Mutex<Option<Box<dyn FnOnce() + Send>>>> = OnceLock::new();
static SHUTDOWN: OnceLock<Mutex<Option<Box<dyn FnOnce() + Send>>>> = OnceLock::new();
static A11Y_ENABLED: AtomicBool = AtomicBool::new(true);
fn set_accessibility_enabled(enabled: bool) {
A11Y_ENABLED.store(enabled, AtomicOrdering::SeqCst);
}
pub(crate) fn accessibility_enabled() -> bool {
A11Y_ENABLED.load(AtomicOrdering::SeqCst)
}
fn with_startup<F, R>(f: F) -> R
where
F: FnOnce(&mut Option<Box<dyn FnOnce() + Send>>) -> R,
{
let lock = STARTUP.get_or_init(|| Mutex::new(None));
let mut guard = lock.lock().unwrap_or_else(|e| e.into_inner());
f(&mut guard)
}
fn with_shutdown<F, R>(f: F) -> R
where
F: FnOnce(&mut Option<Box<dyn FnOnce() + Send>>) -> R,
{
let lock = SHUTDOWN.get_or_init(|| Mutex::new(None));
let mut guard = lock.lock().unwrap_or_else(|e| e.into_inner());
f(&mut guard)
}
fn fire_startup() {
let cb = with_startup(|slot| slot.take());
if let Some(cb) = cb {
cb();
}
}
fn fire_shutdown() {
let cb = with_shutdown(|slot| slot.take());
if let Some(cb) = cb {
cb();
}
}
fn trace_runtime_route(stage: &str) {
crate::trace_runtime_route(stage);
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
#[test]
fn a_startup_callback_can_reenter_registration() {
let calls = Arc::new(AtomicU32::new(0));
let c1 = Arc::clone(&calls);
let c2 = Arc::clone(&calls);
let _app = App::new().on_startup(move || {
c1.fetch_add(1, Ordering::SeqCst);
let c3 = Arc::clone(&c2);
let _ = App::new().on_startup(move || {
c3.fetch_add(10, Ordering::SeqCst);
});
});
fire_startup();
assert_eq!(calls.load(Ordering::SeqCst), 1, "the outer startup callback runs");
fire_startup();
assert_eq!(calls.load(Ordering::SeqCst), 11, "the re-registered callback runs later");
}
#[test]
fn a_shutdown_callback_can_reenter_registration() {
let calls = Arc::new(AtomicU32::new(0));
let c1 = Arc::clone(&calls);
let c2 = Arc::clone(&calls);
let _app = App::new().on_shutdown(move || {
c1.fetch_add(1, Ordering::SeqCst);
let c3 = Arc::clone(&c2);
let _ = App::new().on_shutdown(move || {
c3.fetch_add(10, Ordering::SeqCst);
});
});
fire_shutdown();
assert_eq!(calls.load(Ordering::SeqCst), 1, "the outer shutdown callback runs");
fire_shutdown();
assert_eq!(calls.load(Ordering::SeqCst), 11, "the re-registered callback runs later");
}
fn app_init_test_lock() -> std::sync::MutexGuard<'static, ()> {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
#[test]
fn with_i18n_false_skips_i18n_initialisation() {
let _lock = app_init_test_lock();
let _i18n = crate::i18n::global_i18n_test_lock();
*crate::compat::lock(&crate::i18n::GLOBAL_I18N) = None;
let mut app = App::with_config(AppConfig::default().with_i18n(false));
app.init();
assert!(
crate::i18n::GLOBAL_I18N.lock().unwrap_or_else(|e| e.into_inner()).is_none(),
"with_i18n(false) must leave the i18n manager uninitialised"
);
}
#[test]
fn with_i18n_true_initialises_i18n() {
let _lock = app_init_test_lock();
let _i18n = crate::i18n::global_i18n_test_lock();
*crate::compat::lock(&crate::i18n::GLOBAL_I18N) = None;
let mut app = App::new();
app.init();
let count = crate::i18n::GLOBAL_I18N
.lock()
.unwrap_or_else(|e| e.into_inner())
.as_ref()
.map(|manager| manager.translation_count())
.unwrap_or(0);
assert!(count > 0, "the default configuration must initialise the embedded catalogue");
*crate::compat::lock(&crate::i18n::GLOBAL_I18N) = None;
}
#[test]
fn accessibility_switch_is_recorded_by_init() {
let _lock = app_init_test_lock();
let _i18n = crate::i18n::global_i18n_test_lock();
let mut off =
App::with_config(AppConfig::default().with_accessibility(false).with_i18n(false));
off.init();
assert!(!off.accessibility_enabled(), "with_accessibility(false) must be recorded off");
assert!(!accessibility_enabled(), "the process-wide flag must reflect the config");
let mut on =
App::with_config(AppConfig::default().with_accessibility(true).with_i18n(false));
on.init();
assert!(on.accessibility_enabled(), "the default/true switch must be recorded on");
assert!(accessibility_enabled());
}
}