Skip to main content

wavecraft_dev_server/
webview.rs

1//! WebView setup and event loop
2//!
3//! This module creates the desktop window, configures the WebView with
4//! embedded assets, sets up IPC communication, and runs the event loop.
5
6use crate::app::AppState;
7use crate::assets;
8use std::borrow::Cow;
9use std::sync::Arc;
10use tao::{
11    event::{Event, WindowEvent},
12    event_loop::{ControlFlow, EventLoop},
13    window::WindowBuilder,
14};
15use tracing::{debug, error, warn};
16use wavecraft_bridge::IpcHandler;
17use wry::WebViewBuilder;
18
19/// IPC primitives JavaScript (injected before React loads)
20const IPC_PRIMITIVES_JS: &str = include_str!("js/ipc-primitives.js");
21
22/// Run the desktop application
23///
24/// Creates a window, embeds the React UI via WebView, and handles IPC communication.
25pub fn run_app(state: Arc<AppState>) -> Result<(), Box<dyn std::error::Error>> {
26    // Create event loop
27    let event_loop = EventLoop::new();
28
29    // Create window
30    let window = WindowBuilder::new()
31        .with_title("VstKit Desktop POC")
32        .with_inner_size(tao::dpi::LogicalSize::new(800, 600))
33        .build(&event_loop)
34        .map_err(|e| format!("Failed to create window: {}", e))?;
35
36    // Create IPC handler (unwrap Arc to pass AppState directly)
37    let handler = IpcHandler::new((*state).clone());
38
39    // Keep a reference to state for potential future use
40    let _state = state;
41
42    // Create channel for sending responses back to event loop
43    let (response_tx, response_rx) = std::sync::mpsc::channel::<String>();
44
45    // Create WebView
46    let webview = WebViewBuilder::new()
47        // Inject IPC primitives before any other scripts
48        .with_initialization_script(IPC_PRIMITIVES_JS)
49        // Register custom protocol for embedded assets
50        .with_custom_protocol("vstkit".to_string(), move |_webview, request| {
51            handle_asset_request(request)
52        })
53        // Handle IPC messages from UI
54        .with_ipc_handler(move |request: wry::http::Request<String>| {
55            let message = request.body();
56            let response = handler.handle_json(message);
57
58            // Log for debugging
59            debug!("IPC Request: {}", message);
60            debug!("IPC Response: {}", response);
61
62            // Send response through channel to be delivered in event loop
63            let _ = response_tx.send(response);
64        })
65        .build(&window)?;
66
67    // Navigate to the React app
68    webview.load_url("vstkit://localhost/index.html")?;
69
70    // Run event loop
71    event_loop.run(move |event, _, control_flow| {
72        // Poll continuously to process IPC responses
73        *control_flow = ControlFlow::Poll;
74
75        // Process any pending IPC responses
76        while let Ok(response) = response_rx.try_recv() {
77            // Escape the JSON response for JavaScript
78            let escaped_response = response
79                .replace('\\', "\\\\")
80                .replace('\'', "\\'")
81                .replace('\n', "\\n")
82                .replace('\r', "\\r");
83
84            // Call the internal _receive method that the primitives expose
85            let js_code = format!(
86                "globalThis.__WAVECRAFT_IPC__._receive('{}');",
87                escaped_response
88            );
89
90            if let Err(e) = webview.evaluate_script(&js_code) {
91                error!("Failed to send response to UI: {}", e);
92            }
93        }
94
95        if let Event::WindowEvent {
96            event: WindowEvent::CloseRequested,
97            ..
98        } = event
99        {
100            *control_flow = ControlFlow::Exit;
101        }
102    });
103}
104
105/// Handle requests for embedded assets via custom protocol
106fn handle_asset_request(
107    request: wry::http::Request<Vec<u8>>,
108) -> wry::http::Response<Cow<'static, [u8]>> {
109    let path = request.uri().path();
110
111    match assets::get_asset(path) {
112        Some((content, mime_type)) => wry::http::Response::builder()
113            .status(200)
114            .header("Content-Type", mime_type)
115            .header("Access-Control-Allow-Origin", "*")
116            .body(content)
117            .unwrap(),
118        None => {
119            warn!("Asset not found: {}", path);
120            wry::http::Response::builder()
121                .status(404)
122                .body(Cow::Borrowed(b"Not Found" as &[u8]))
123                .unwrap()
124        }
125    }
126}