Investigation of a tech stack for a future project
at main 219 lines 7.2 kB view raw
1use eframe::WebRunner; 2use js_sys::Promise; 3use serde::{Deserialize, Serialize}; 4use std::cell::RefCell; 5use std::rc::Rc; 6use wasm_bindgen::JsCast; 7use wasm_bindgen::prelude::*; 8use web_sys::HtmlCanvasElement; 9 10#[wasm_bindgen(module = "/js/thrift-client.js")] 11extern "C" { 12 fn connect(port: u16) -> Promise; 13 fn getInfo() -> Promise; 14 fn isConnected() -> bool; 15 #[wasm_bindgen(js_name = setDisconnectCallback)] 16 fn set_disconnect_callback(callback: &Closure<dyn FnMut()>); 17} 18 19#[derive(Serialize, Deserialize, Clone)] 20pub struct ServiceInfo { 21 pub name: String, 22 pub version: String, 23 pub status: String, 24} 25 26#[wasm_bindgen] 27pub struct WebHandle { 28 runner: WebRunner, 29} 30 31#[wasm_bindgen] 32impl WebHandle { 33 pub async fn start(&self, canvas: HtmlCanvasElement) -> Result<(), JsValue> { 34 self.runner 35 .start( 36 canvas, 37 eframe::WebOptions::default(), 38 Box::new(|cc| Ok(Box::new(HelloHmiApp::new(cc)))), 39 ) 40 .await 41 } 42} 43 44enum ConnectionState { 45 Disconnected, 46 Connecting, 47 Connected(Option<ServiceInfo>), 48 FetchingInfo, 49} 50 51impl Default for ConnectionState { 52 fn default() -> Self { 53 Self::Disconnected 54 } 55} 56 57struct AppState { 58 connection: ConnectionState, 59 error: Option<String>, 60} 61 62impl Default for AppState { 63 fn default() -> Self { 64 Self { 65 connection: ConnectionState::Disconnected, 66 error: None, 67 } 68 } 69} 70 71struct HelloHmiApp { 72 state: Rc<RefCell<AppState>>, 73 #[allow(dead_code)] 74 disconnect_callback: Closure<dyn FnMut()>, 75} 76 77impl HelloHmiApp { 78 fn new(_cc: &eframe::CreationContext<'_>) -> Self { 79 let state = Rc::new(RefCell::new(AppState::default())); 80 81 let state_clone = state.clone(); 82 let disconnect_callback = Closure::wrap(Box::new(move || { 83 let mut s = state_clone.borrow_mut(); 84 s.connection = ConnectionState::Disconnected; 85 s.error = Some("Connection lost".to_string()); 86 }) as Box<dyn FnMut()>); 87 88 set_disconnect_callback(&disconnect_callback); 89 90 Self { 91 state, 92 disconnect_callback, 93 } 94 } 95} 96 97impl eframe::App for HelloHmiApp { 98 fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 99 let mut state = self.state.borrow_mut(); 100 101 if matches!(state.connection, ConnectionState::Connected(_)) && !isConnected() { 102 state.connection = ConnectionState::Disconnected; 103 state.error = Some("Connection lost".to_string()); 104 } 105 106 egui::CentralPanel::default().show(ctx, |ui| { 107 ui.heading("HMI Client"); 108 109 ui.separator(); 110 111 match &mut state.connection { 112 ConnectionState::Disconnected => { 113 if let Some(ref error) = state.error { 114 ui.colored_label(egui::Color32::RED, error); 115 ui.separator(); 116 } 117 118 if ui.button("Connect to Server").clicked() { 119 state.connection = ConnectionState::Connecting; 120 state.error = None; 121 122 let state_clone = self.state.clone(); 123 124 wasm_bindgen_futures::spawn_local(async move { 125 let promise = connect(9090); 126 let future = wasm_bindgen_futures::JsFuture::from(promise); 127 128 match future.await { 129 Ok(_) => { 130 let mut s = state_clone.borrow_mut(); 131 s.connection = ConnectionState::Connected(None); 132 } 133 Err(e) => { 134 let mut s = state_clone.borrow_mut(); 135 s.connection = ConnectionState::Disconnected; 136 s.error = Some(format!("Connection failed: {:?}", e)); 137 } 138 } 139 }); 140 } 141 } 142 143 ConnectionState::Connecting => { 144 ui.spinner(); 145 ui.label("Connecting..."); 146 } 147 148 ConnectionState::FetchingInfo => { 149 ui.spinner(); 150 ui.label("Fetching info..."); 151 } 152 153 ConnectionState::Connected(service_info) => { 154 ui.label("Connected!"); 155 ui.separator(); 156 157 if let &mut Some(ref info) = service_info { 158 ui.label(format!("Name: {}", info.name)); 159 ui.label(format!("Version: {}", info.version)); 160 ui.label(format!("Status: {}", info.status)); 161 } else { 162 ui.label("Click 'Get Info' to fetch service information."); 163 } 164 165 if ui.button("Get Info").clicked() { 166 state.connection = ConnectionState::FetchingInfo; 167 state.error = None; 168 169 let state_clone = self.state.clone(); 170 171 wasm_bindgen_futures::spawn_local(async move { 172 let promise = getInfo(); 173 let future = wasm_bindgen_futures::JsFuture::from(promise); 174 175 match future.await { 176 Ok(value) => { 177 let mut s = state_clone.borrow_mut(); 178 if let Ok(info) = 179 serde_wasm_bindgen::from_value::<ServiceInfo>(value) 180 { 181 s.connection = ConnectionState::Connected(Some(info)); 182 } 183 } 184 Err(e) => { 185 let mut s = state_clone.borrow_mut(); 186 s.connection = ConnectionState::Connected(None); 187 s.error = Some(format!("Get info failed: {:?}", e)); 188 } 189 } 190 }); 191 } 192 } 193 } 194 195 if let Some(ref error) = state.error { 196 if !matches!(state.connection, ConnectionState::Disconnected) { 197 ui.colored_label(egui::Color32::RED, error); 198 } 199 } 200 }); 201 202 ctx.request_repaint(); 203 } 204} 205 206#[wasm_bindgen(start)] 207pub async fn main() -> Result<(), JsValue> { 208 let window = web_sys::window().expect("no global `window` exists"); 209 let document = window.document().expect("should have a document on window"); 210 let canvas = document 211 .get_element_by_id("eframe") 212 .expect("document should have #eframe canvas") 213 .dyn_into::<HtmlCanvasElement>()?; 214 215 let web_handle = WebHandle { 216 runner: WebRunner::new(), 217 }; 218 web_handle.start(canvas).await 219}