use eframe::WebRunner; use js_sys::Promise; use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::rc::Rc; use wasm_bindgen::JsCast; use wasm_bindgen::prelude::*; use web_sys::HtmlCanvasElement; #[wasm_bindgen(module = "/js/thrift-client.js")] extern "C" { fn connect(port: u16) -> Promise; fn getInfo() -> Promise; fn isConnected() -> bool; #[wasm_bindgen(js_name = setDisconnectCallback)] fn set_disconnect_callback(callback: &Closure); } #[derive(Serialize, Deserialize, Clone)] pub struct ServiceInfo { pub name: String, pub version: String, pub status: String, } #[wasm_bindgen] pub struct WebHandle { runner: WebRunner, } #[wasm_bindgen] impl WebHandle { pub async fn start(&self, canvas: HtmlCanvasElement) -> Result<(), JsValue> { self.runner .start( canvas, eframe::WebOptions::default(), Box::new(|cc| Ok(Box::new(HelloHmiApp::new(cc)))), ) .await } } enum ConnectionState { Disconnected, Connecting, Connected(Option), FetchingInfo, } impl Default for ConnectionState { fn default() -> Self { Self::Disconnected } } struct AppState { connection: ConnectionState, error: Option, } impl Default for AppState { fn default() -> Self { Self { connection: ConnectionState::Disconnected, error: None, } } } struct HelloHmiApp { state: Rc>, #[allow(dead_code)] disconnect_callback: Closure, } impl HelloHmiApp { fn new(_cc: &eframe::CreationContext<'_>) -> Self { let state = Rc::new(RefCell::new(AppState::default())); let state_clone = state.clone(); let disconnect_callback = Closure::wrap(Box::new(move || { let mut s = state_clone.borrow_mut(); s.connection = ConnectionState::Disconnected; s.error = Some("Connection lost".to_string()); }) as Box); set_disconnect_callback(&disconnect_callback); Self { state, disconnect_callback, } } } impl eframe::App for HelloHmiApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { let mut state = self.state.borrow_mut(); if matches!(state.connection, ConnectionState::Connected(_)) && !isConnected() { state.connection = ConnectionState::Disconnected; state.error = Some("Connection lost".to_string()); } egui::CentralPanel::default().show(ctx, |ui| { ui.heading("HMI Client"); ui.separator(); match &mut state.connection { ConnectionState::Disconnected => { if let Some(ref error) = state.error { ui.colored_label(egui::Color32::RED, error); ui.separator(); } if ui.button("Connect to Server").clicked() { state.connection = ConnectionState::Connecting; state.error = None; let state_clone = self.state.clone(); wasm_bindgen_futures::spawn_local(async move { let promise = connect(9090); let future = wasm_bindgen_futures::JsFuture::from(promise); match future.await { Ok(_) => { let mut s = state_clone.borrow_mut(); s.connection = ConnectionState::Connected(None); } Err(e) => { let mut s = state_clone.borrow_mut(); s.connection = ConnectionState::Disconnected; s.error = Some(format!("Connection failed: {:?}", e)); } } }); } } ConnectionState::Connecting => { ui.spinner(); ui.label("Connecting..."); } ConnectionState::FetchingInfo => { ui.spinner(); ui.label("Fetching info..."); } ConnectionState::Connected(service_info) => { ui.label("Connected!"); ui.separator(); if let &mut Some(ref info) = service_info { ui.label(format!("Name: {}", info.name)); ui.label(format!("Version: {}", info.version)); ui.label(format!("Status: {}", info.status)); } else { ui.label("Click 'Get Info' to fetch service information."); } if ui.button("Get Info").clicked() { state.connection = ConnectionState::FetchingInfo; state.error = None; let state_clone = self.state.clone(); wasm_bindgen_futures::spawn_local(async move { let promise = getInfo(); let future = wasm_bindgen_futures::JsFuture::from(promise); match future.await { Ok(value) => { let mut s = state_clone.borrow_mut(); if let Ok(info) = serde_wasm_bindgen::from_value::(value) { s.connection = ConnectionState::Connected(Some(info)); } } Err(e) => { let mut s = state_clone.borrow_mut(); s.connection = ConnectionState::Connected(None); s.error = Some(format!("Get info failed: {:?}", e)); } } }); } } } if let Some(ref error) = state.error { if !matches!(state.connection, ConnectionState::Disconnected) { ui.colored_label(egui::Color32::RED, error); } } }); ctx.request_repaint(); } } #[wasm_bindgen(start)] pub async fn main() -> Result<(), JsValue> { let window = web_sys::window().expect("no global `window` exists"); let document = window.document().expect("should have a document on window"); let canvas = document .get_element_by_id("eframe") .expect("document should have #eframe canvas") .dyn_into::()?; let web_handle = WebHandle { runner: WebRunner::new(), }; web_handle.start(canvas).await }