Investigation of a tech stack for a future project
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}