Files
smart-rgb/crates/borders-desktop/src/plugin.rs
2025-10-11 14:52:54 -05:00

177 lines
6.8 KiB
Rust

//! Tauri-Bevy integration plugin
//!
//! This module provides the main integration between Tauri and Bevy, handling
//! the main application loop and event bridging.
use borders_core::app::{App, Plugin, Update};
use borders_core::time::Time;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tauri::{Manager, RunEvent};
#[cfg(not(target_arch = "wasm32"))]
use std::time::Instant;
#[cfg(target_arch = "wasm32")]
use web_time::Instant;
use crate::render_bridge::{TauriRenderBridgeTransport, cache_leaderboard_snapshot_system};
const TARGET_FPS: f64 = 60.0;
pub fn generate_tauri_context() -> tauri::Context {
tauri::generate_context!()
}
fn setup_tauri_integration(app: &mut App, tauri_app: &tauri::AppHandle, shared_render_state: Arc<Mutex<Option<borders_core::ui::protocol::RenderInit>>>, shared_leaderboard_state: Arc<Mutex<Option<borders_core::ui::protocol::LeaderboardSnapshot>>>) {
tracing::debug!("Setup tauri integration");
// Register state for render bridge commands
tauri_app.manage(Arc::new(Mutex::new(None::<borders_core::ui::protocol::CameraStateUpdate>)));
tauri_app.manage(Arc::new(Mutex::new(None::<borders_core::networking::GameView>)));
// InputState - shared between Tauri commands and ECS systems
let input_state_shared = Arc::new(Mutex::new(borders_core::ui::input::InputState::new()));
tauri_app.manage(input_state_shared.clone());
app.insert_non_send_resource(input_state_shared);
// Register shared state with Tauri (for get_game_state command)
tauri_app.manage(shared_render_state.clone());
tauri_app.manage(shared_leaderboard_state.clone());
// Get the message queue from the transport (already added as plugin)
let transport = app.world().get_resource::<borders_core::ui::RenderBridge<TauriRenderBridgeTransport>>().expect("RenderBridge should be added by plugin");
let message_queue = transport.transport.inbound_messages();
tauri_app.manage(message_queue);
// Store shared states in world
app.insert_non_send_resource(shared_leaderboard_state);
}
pub struct TauriPlugin {
setup: Box<dyn Fn() -> tauri::App + Send + Sync>,
}
impl TauriPlugin {
pub fn new<F>(setup: F) -> Self
where
F: Fn() -> tauri::App + Send + Sync + 'static,
{
Self { setup: Box::new(setup) }
}
}
impl TauriPlugin {
pub fn build_and_run(self, mut app: App) -> ! {
let tauri_app = (self.setup)();
// Create shared state for game state recovery
let shared_render_state = Arc::new(Mutex::new(None::<borders_core::ui::protocol::RenderInit>));
let shared_leaderboard_state = Arc::new(Mutex::new(None::<borders_core::ui::protocol::LeaderboardSnapshot>));
// Create transport for Tauri frontend (handles both render and UI communication)
let transport = TauriRenderBridgeTransport::new(tauri_app.handle().clone(), shared_render_state.clone());
// Add the render bridge plugin to handle all frontend communication
borders_core::ui::FrontendPlugin::new(transport).build(&mut app);
// Set up Tauri integration directly (no startup system needed)
setup_tauri_integration(&mut app, tauri_app.handle(), shared_render_state, shared_leaderboard_state);
// Add the leaderboard caching system
app.add_systems(Update, cache_leaderboard_snapshot_system);
// Run the app
run_tauri_app(app, tauri_app);
std::process::exit(0)
}
}
pub fn run_tauri_app(app: App, tauri_app: tauri::App) {
let app_rc = Rc::new(RefCell::new(app));
let mut tauri_app = tauri_app;
let mut is_initialized = false;
let mut last_frame_time = Instant::now();
let target_frame_duration = Duration::from_secs_f64(1.0 / TARGET_FPS);
loop {
let frame_start = Instant::now();
#[allow(deprecated)]
tauri_app.run_iteration(move |_app_handle, event: RunEvent| {
match event {
tauri::RunEvent::Ready => {
// Event acknowledged, actual setup happens below
}
tauri::RunEvent::ExitRequested { .. } => {
// Track session end and flush analytics before exit
if borders_core::telemetry::client().is_some() {
tracing::debug!("ExitRequested: tracking session end and flushing analytics");
// Create a minimal runtime for blocking operations
let runtime = tokio::runtime::Builder::new_current_thread().enable_time().enable_io().build().expect("Failed to create tokio runtime for flush");
runtime.block_on(async {
// Track session end event
borders_core::telemetry::track_session_end().await;
// Flush all pending events (the batch-triggered send is now synchronous)
if let Some(client) = borders_core::telemetry::client() {
let timeout = std::time::Duration::from_millis(500);
match tokio::time::timeout(timeout, client.flush()).await {
Ok(_) => {
tracing::debug!("Analytics flushed successfully before exit")
}
Err(_) => {
tracing::warn!("Analytics flush timed out after 500ms")
}
}
}
});
}
}
_ => (),
}
});
if tauri_app.webview_windows().is_empty() {
tauri_app.cleanup_before_exit();
break;
}
// Initialize game plugin on first iteration after Tauri is ready
if !is_initialized {
let mut app = app_rc.borrow_mut();
// Add core game plugin
borders_core::GamePlugin::new(borders_core::plugin::NetworkMode::Local).build(&mut app);
app.run_startup();
app.finish();
app.cleanup();
is_initialized = true;
last_frame_time = Instant::now(); // Reset timer after initialization
tracing::info!("Game initialized");
}
// Update time resource with delta from PREVIOUS frame
let mut app = app_rc.borrow_mut();
let delta = frame_start.duration_since(last_frame_time);
if let Some(mut time) = app.world_mut().get_resource_mut::<Time>() {
time.update(delta);
}
app.update();
let frame_duration = frame_start.elapsed();
if frame_duration < target_frame_duration {
std::thread::sleep(target_frame_duration - frame_duration);
}
last_frame_time = frame_start;
}
}