// file: kb-app-demo-desktop/src/splash.rs // version: 6 //! Splash-window payloads and startup sequencing helpers. use tauri::Emitter; // rust-rules: trait-import use tauri::Manager; // rust-rules: trait-import use ts_rs::TS; // rust-rules: derive-import /// Minimum splash duration before the main window is shown. pub(crate) const SPLASH_MINIMUM_MS: u64 = 12100; /// Splash fade duration. pub(crate) const SPLASH_FADE_MS: u32 = 12000; /// Delay after fade-out before destroying the splash window. pub(crate) const SPLASH_CLOSE_WAIT_MS: u64 = 12100; /// Command sent from Rust to the splash frontend. #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)] #[ts( export, export_to = "../frontend/ts/bindings/kb_app_demo_desktop/splash/SplashOrder.ts" )] pub(crate) struct SplashOrder { /// Command name. pub(crate) order: std::string::String, /// Optional message. pub(crate) msg: std::option::Option, /// Optional status. pub(crate) status: std::option::Option, /// Optional animation duration in milliseconds. pub(crate) duration_ms: std::option::Option, } /// Emits one splash command. pub(crate) fn emit_splash_order( splash_window: &tauri::WebviewWindow, order: &str, msg: std::option::Option<&str>, status: std::option::Option<&str>, duration_ms: std::option::Option, ) { let payload = crate::SplashOrder { order: order.to_string(), msg: msg.map(std::string::ToString::to_string), status: status.map(std::string::ToString::to_string), duration_ms, }; let emit_result = splash_window.emit("splash", payload); if let std::result::Result::Err(error) = emit_result { tracing::error!(target: crate::TRACING_TARGET, "cannot emit splash order '{order}': {error:?}"); } } /// Runs the startup sequence after the splash frontend installed its listener. pub(crate) async fn splash_frontend_ready( app: tauri::AppHandle, window: tauri::WebviewWindow, state: &crate::AppState, ) -> std::result::Result<(), std::string::String> { if window.label() != "splash" { return std::result::Result::Err( "splash readiness may only originate from splash".to_string(), ); } if !state.begin_startup_sequence() { return std::result::Result::Ok(()); } let main_window = match app.get_webview_window("main") { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err("main window is missing".to_string()); }, }; let started_at = tokio::time::Instant::now(); crate::emit_splash_order( &window, "fadein", std::option::Option::None, std::option::Option::None, std::option::Option::Some(crate::SPLASH_FADE_MS), ); crate::emit_splash_order( &window, "add_msg", std::option::Option::Some("Chargement de la configuration terminé."), std::option::Option::Some("success"), std::option::Option::None, ); crate::emit_splash_order( &window, "add_log", std::option::Option::Some( format!("Profil actif : {}", state.active_profile().name).as_str(), ), std::option::Option::None, std::option::Option::None, ); crate::emit_splash_order( &window, "add_msg", std::option::Option::Some( format!("Logging initialisé : {} routes actives.", state.logging_route_count()) .as_str(), ), std::option::Option::Some("success"), std::option::Option::None, ); crate::emit_splash_order( &window, "add_msg", std::option::Option::Some("Pool HTTP initialisé."), std::option::Option::Some("success"), std::option::Option::None, ); crate::initialize_store_for_startup(state, &window).await; crate::emit_splash_order( &window, "add_msg", std::option::Option::Some("Initialisation terminée."), std::option::Option::Some("success"), std::option::Option::None, ); wait_until_minimum(started_at, crate::SPLASH_MINIMUM_MS).await; crate::emit_splash_order( &window, "fadeout", std::option::Option::None, std::option::Option::None, std::option::Option::Some(crate::SPLASH_FADE_MS), ); tokio::time::sleep(std::time::Duration::from_millis(crate::SPLASH_CLOSE_WAIT_MS)).await; if let std::result::Result::Err(error) = main_window.show() { return std::result::Result::Err(format!("cannot show main window: {error:?}")); } if let std::result::Result::Err(error) = main_window.set_focus() { return std::result::Result::Err(format!("cannot focus main window: {error:?}")); } if let std::result::Result::Err(error) = window.destroy() { return std::result::Result::Err(format!("cannot destroy splash window: {error:?}")); } return std::result::Result::Ok(()); } /// Waits until the configured minimum splash duration has elapsed. async fn wait_until_minimum(start: tokio::time::Instant, minimum_ms: u64) { let minimum = std::time::Duration::from_millis(minimum_ms); let elapsed = start.elapsed(); if elapsed >= minimum { return; } tokio::time::sleep(minimum - elapsed).await; } #[cfg(test)] mod tests { #[test] fn splash_order_serializes_duration_ms() { let order = crate::SplashOrder { order: "fadein".to_string(), msg: std::option::Option::None, status: std::option::Option::None, duration_ms: std::option::Option::Some(500), }; let value_result = serde_json::to_value(order); let value = match value_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("cannot serialize splash order: {error}"), }; assert_eq!(value["duration_ms"], serde_json::json!(500)); } #[test] fn splash_timing_is_ordered() { assert_eq!(crate::SPLASH_MINIMUM_MS, 12100); assert_eq!(crate::SPLASH_FADE_MS, 12000); assert!(crate::SPLASH_CLOSE_WAIT_MS >= u64::from(crate::SPLASH_FADE_MS)); } }