1use crate::plots::Figure;
8use std::sync::{mpsc, Arc, Mutex, OnceLock};
9use std::thread::{self, ThreadId};
10
11#[derive(Debug)]
13pub enum GuiThreadMessage {
14 ShowPlot {
16 figure: Figure,
17 response: mpsc::Sender<GuiOperationResult>,
18 },
19 CloseAll {
21 response: mpsc::Sender<GuiOperationResult>,
22 },
23 HealthCheck {
25 response: mpsc::Sender<GuiOperationResult>,
26 },
27 Shutdown,
29}
30
31#[derive(Debug, Clone)]
33pub enum GuiOperationResult {
34 Success(String),
36 Error {
38 message: String,
39 error_code: GuiErrorCode,
40 recoverable: bool,
41 },
42 Cancelled(String),
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum GuiErrorCode {
49 EventLoopCreationFailed,
51 WindowCreationFailed,
53 WgpuInitializationFailed,
55 ThreadCommunicationFailed,
57 MainThreadViolation,
59 ResourceExhaustion,
61 InvalidState,
63 PlatformError,
65 Unknown,
67}
68
69impl std::fmt::Display for GuiOperationResult {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 match self {
72 GuiOperationResult::Success(msg) => write!(f, "Success: {msg}"),
73 GuiOperationResult::Error {
74 message,
75 error_code,
76 recoverable,
77 } => {
78 write!(
79 f,
80 "Error [{error_code:?}]: {message} (recoverable: {recoverable})"
81 )
82 }
83 GuiOperationResult::Cancelled(msg) => write!(f, "Cancelled: {msg}"),
84 }
85 }
86}
87
88impl std::error::Error for GuiOperationResult {}
89
90pub struct MainThreadDetector {
92 main_thread_id: OnceLock<ThreadId>,
93}
94
95impl Default for MainThreadDetector {
96 fn default() -> Self {
97 Self::new()
98 }
99}
100
101impl MainThreadDetector {
102 pub const fn new() -> Self {
104 Self {
105 main_thread_id: OnceLock::new(),
106 }
107 }
108
109 pub fn register_main_thread(&self) {
112 let current_id = thread::current().id();
113 if self.main_thread_id.set(current_id).is_err() {
114 }
116 }
117
118 pub fn is_main_thread(&self) -> bool {
120 if let Some(main_id) = self.main_thread_id.get() {
121 thread::current().id() == *main_id
122 } else {
123 self.register_main_thread();
125 true
126 }
127 }
128
129 pub fn main_thread_id(&self) -> Option<ThreadId> {
131 self.main_thread_id.get().copied()
132 }
133}
134
135static MAIN_THREAD_DETECTOR: MainThreadDetector = MainThreadDetector::new();
137
138pub fn register_main_thread() {
140 MAIN_THREAD_DETECTOR.register_main_thread();
141}
142
143pub fn is_main_thread() -> bool {
145 MAIN_THREAD_DETECTOR.is_main_thread()
146}
147
148pub struct GuiThreadManager {
150 sender: mpsc::Sender<GuiThreadMessage>,
152 thread_handle: Option<thread::JoinHandle<Result<(), GuiOperationResult>>>,
154 health_state: Arc<Mutex<GuiHealthState>>,
156}
157
158#[derive(Debug, Clone)]
160struct GuiHealthState {
161 last_response: std::time::Instant,
162 response_count: u64,
163 error_count: u64,
164 is_healthy: bool,
165}
166
167impl GuiThreadManager {
168 pub fn new() -> Result<Self, GuiOperationResult> {
172 #[cfg(target_os = "macos")]
174 if !is_main_thread() {
175 return Err(GuiOperationResult::Error {
176 message: "GuiThreadManager must be created on the main thread on macOS".to_string(),
177 error_code: GuiErrorCode::MainThreadViolation,
178 recoverable: false,
179 });
180 }
181
182 let (sender, receiver) = mpsc::channel();
183 let health_state = Arc::new(Mutex::new(GuiHealthState {
184 last_response: std::time::Instant::now(),
185 response_count: 0,
186 error_count: 0,
187 is_healthy: true,
188 }));
189
190 let health_state_clone = Arc::clone(&health_state);
191
192 let thread_handle = thread::Builder::new()
194 .name("runmat-gui".to_string())
195 .spawn(move || Self::gui_thread_main(receiver, health_state_clone))
196 .map_err(|e| GuiOperationResult::Error {
197 message: format!("Failed to spawn GUI thread: {e}"),
198 error_code: GuiErrorCode::ThreadCommunicationFailed,
199 recoverable: false,
200 })?;
201
202 Ok(Self {
203 sender,
204 thread_handle: Some(thread_handle),
205 health_state,
206 })
207 }
208
209 fn gui_thread_main(
211 receiver: mpsc::Receiver<GuiThreadMessage>,
212 health_state: Arc<Mutex<GuiHealthState>>,
213 ) -> Result<(), GuiOperationResult> {
214 log::info!("GUI thread started successfully");
215
216 #[cfg(feature = "gui")]
218 let gui_context = Self::initialize_gui_context()?;
219
220 loop {
221 match receiver.recv() {
222 Ok(message) => {
223 let result = Self::handle_gui_message(message, &gui_context);
224
225 if let Ok(mut health) = health_state.lock() {
227 health.last_response = std::time::Instant::now();
228 health.response_count += 1;
229
230 if let Some(GuiOperationResult::Error { .. }) = &result {
231 health.error_count += 1;
232 health.is_healthy = health.error_count < 10; }
234 }
235
236 if result.is_none() {
238 break;
239 }
240 }
241 Err(_) => {
242 log::info!("GUI thread channel closed, shutting down");
244 break;
245 }
246 }
247 }
248
249 log::info!("GUI thread exiting gracefully");
250 Ok(())
251 }
252
253 #[cfg(feature = "gui")]
255 fn initialize_gui_context() -> Result<GuiContext, GuiOperationResult> {
256 use crate::gui::window::WindowConfig;
257
258 Ok(GuiContext {
260 _default_config: WindowConfig::default(),
261 _active_windows: Vec::new(),
262 })
263 }
264
265 #[cfg(not(feature = "gui"))]
266 fn initialize_gui_context() -> Result<GuiContext, GuiOperationResult> {
267 Err(GuiOperationResult::Error {
268 message: "GUI feature not enabled".to_string(),
269 error_code: GuiErrorCode::InvalidState,
270 recoverable: false,
271 })
272 }
273
274 fn handle_gui_message(
276 message: GuiThreadMessage,
277 gui_context: &GuiContext,
278 ) -> Option<GuiOperationResult> {
279 match message {
280 GuiThreadMessage::ShowPlot { figure, response } => {
281 let result = Self::handle_show_plot(figure, gui_context);
282 let _ = response.send(result.clone());
283 Some(result)
284 }
285 GuiThreadMessage::CloseAll { response } => {
286 let result = Self::handle_close_all(gui_context);
287 let _ = response.send(result.clone());
288 Some(result)
289 }
290 GuiThreadMessage::HealthCheck { response } => {
291 let result = GuiOperationResult::Success("GUI thread healthy".to_string());
292 let _ = response.send(result.clone());
293 Some(result)
294 }
295 GuiThreadMessage::Shutdown => {
296 log::info!("GUI thread received shutdown signal");
297 None }
299 }
300 }
301
302 #[cfg(feature = "gui")]
304 fn handle_show_plot(figure: Figure, _gui_context: &GuiContext) -> GuiOperationResult {
305 use crate::gui::{window::WindowConfig, PlotWindow};
306
307 let rt = match tokio::runtime::Runtime::new() {
309 Ok(rt) => rt,
310 Err(e) => {
311 return GuiOperationResult::Error {
312 message: format!("Failed to create async runtime: {e}"),
313 error_code: GuiErrorCode::ResourceExhaustion,
314 recoverable: true,
315 }
316 }
317 };
318
319 rt.block_on(async {
320 let config = WindowConfig::default();
321 let mut window = match PlotWindow::new(config).await {
322 Ok(window) => window,
323 Err(e) => {
324 return GuiOperationResult::Error {
325 message: format!("Failed to create window: {e}"),
326 error_code: GuiErrorCode::WindowCreationFailed,
327 recoverable: true,
328 }
329 }
330 };
331
332 window.set_figure(figure);
334
335 match window.run().await {
337 Ok(_) => GuiOperationResult::Success("Plot window closed".to_string()),
338 Err(e) => GuiOperationResult::Error {
339 message: format!("Window runtime error: {e}"),
340 error_code: GuiErrorCode::PlatformError,
341 recoverable: true,
342 },
343 }
344 })
345 }
346
347 #[cfg(not(feature = "gui"))]
348 fn handle_show_plot(_figure: Figure, _gui_context: &GuiContext) -> GuiOperationResult {
349 GuiOperationResult::Error {
350 message: "GUI feature not enabled".to_string(),
351 error_code: GuiErrorCode::InvalidState,
352 recoverable: false,
353 }
354 }
355
356 fn handle_close_all(_gui_context: &GuiContext) -> GuiOperationResult {
358 GuiOperationResult::Success("All windows closed".to_string())
360 }
361
362 pub fn show_plot(&self, figure: Figure) -> Result<GuiOperationResult, GuiOperationResult> {
364 let (response_tx, response_rx) = mpsc::channel();
365
366 let message = GuiThreadMessage::ShowPlot {
367 figure,
368 response: response_tx,
369 };
370
371 self.sender
373 .send(message)
374 .map_err(|_| GuiOperationResult::Error {
375 message: "GUI thread is not responding".to_string(),
376 error_code: GuiErrorCode::ThreadCommunicationFailed,
377 recoverable: false,
378 })?;
379
380 match response_rx.recv_timeout(std::time::Duration::from_secs(30)) {
382 Ok(result) => Ok(result),
383 Err(mpsc::RecvTimeoutError::Timeout) => Err(GuiOperationResult::Cancelled(
384 "GUI operation timed out after 30 seconds".to_string(),
385 )),
386 Err(mpsc::RecvTimeoutError::Disconnected) => Err(GuiOperationResult::Error {
387 message: "GUI thread disconnected unexpectedly".to_string(),
388 error_code: GuiErrorCode::ThreadCommunicationFailed,
389 recoverable: false,
390 }),
391 }
392 }
393
394 pub fn health_check(&self) -> Result<GuiOperationResult, GuiOperationResult> {
396 let (response_tx, response_rx) = mpsc::channel();
397
398 let message = GuiThreadMessage::HealthCheck {
399 response: response_tx,
400 };
401
402 self.sender
403 .send(message)
404 .map_err(|_| GuiOperationResult::Error {
405 message: "GUI thread is not responding".to_string(),
406 error_code: GuiErrorCode::ThreadCommunicationFailed,
407 recoverable: false,
408 })?;
409
410 match response_rx.recv_timeout(std::time::Duration::from_secs(5)) {
411 Ok(result) => Ok(result),
412 Err(_) => Err(GuiOperationResult::Error {
413 message: "GUI thread health check failed".to_string(),
414 error_code: GuiErrorCode::ThreadCommunicationFailed,
415 recoverable: true,
416 }),
417 }
418 }
419
420 pub fn get_health_state(&self) -> Option<(u64, u64, bool)> {
422 self.health_state
423 .lock()
424 .ok()
425 .map(|health| (health.response_count, health.error_count, health.is_healthy))
426 }
427
428 pub fn shutdown(mut self) -> Result<(), GuiOperationResult> {
430 let _ = self.sender.send(GuiThreadMessage::Shutdown);
432
433 if let Some(handle) = self.thread_handle.take() {
435 match handle.join() {
436 Ok(Ok(())) => Ok(()),
437 Ok(Err(e)) => Err(e),
438 Err(_) => Err(GuiOperationResult::Error {
439 message: "GUI thread panicked during shutdown".to_string(),
440 error_code: GuiErrorCode::PlatformError,
441 recoverable: false,
442 }),
443 }
444 } else {
445 Ok(())
446 }
447 }
448}
449
450impl Drop for GuiThreadManager {
452 fn drop(&mut self) {
453 if self.thread_handle.is_some() {
454 log::warn!("GuiThreadManager dropped without explicit shutdown");
455 let _ = self.sender.send(GuiThreadMessage::Shutdown);
456
457 if let Some(handle) = self.thread_handle.take() {
459 let _ = handle.join();
460 }
461 }
462 }
463}
464
465#[cfg(feature = "gui")]
467struct GuiContext {
468 _default_config: crate::gui::window::WindowConfig,
469 _active_windows: Vec<String>, }
471
472#[cfg(not(feature = "gui"))]
473struct GuiContext {
474 }
476
477static GUI_MANAGER: OnceLock<Arc<Mutex<Option<GuiThreadManager>>>> = OnceLock::new();
479
480pub fn initialize_gui_manager() -> Result<(), GuiOperationResult> {
482 let manager_mutex = GUI_MANAGER.get_or_init(|| Arc::new(Mutex::new(None)));
483
484 let mut manager_guard = manager_mutex
485 .lock()
486 .map_err(|_| GuiOperationResult::Error {
487 message: "Failed to acquire GUI manager lock".to_string(),
488 error_code: GuiErrorCode::ThreadCommunicationFailed,
489 recoverable: false,
490 })?;
491
492 if manager_guard.is_some() {
493 return Ok(()); }
495
496 let manager = GuiThreadManager::new()?;
497 *manager_guard = Some(manager);
498
499 log::info!("Global GUI thread manager initialized successfully");
500 Ok(())
501}
502
503pub fn get_gui_manager() -> Result<Arc<Mutex<Option<GuiThreadManager>>>, GuiOperationResult> {
505 GUI_MANAGER
506 .get()
507 .ok_or_else(|| GuiOperationResult::Error {
508 message: "GUI manager not initialized. Call initialize_gui_manager() first."
509 .to_string(),
510 error_code: GuiErrorCode::InvalidState,
511 recoverable: true,
512 })
513 .map(Arc::clone)
514}
515
516pub fn show_plot_global(figure: Figure) -> Result<GuiOperationResult, GuiOperationResult> {
518 let manager_mutex = get_gui_manager()?;
519 let manager_guard = manager_mutex
520 .lock()
521 .map_err(|_| GuiOperationResult::Error {
522 message: "Failed to acquire GUI manager lock".to_string(),
523 error_code: GuiErrorCode::ThreadCommunicationFailed,
524 recoverable: false,
525 })?;
526
527 match manager_guard.as_ref() {
528 Some(manager) => manager.show_plot(figure),
529 None => Err(GuiOperationResult::Error {
530 message: "GUI manager not initialized".to_string(),
531 error_code: GuiErrorCode::InvalidState,
532 recoverable: true,
533 }),
534 }
535}
536
537pub fn health_check_global() -> Result<GuiOperationResult, GuiOperationResult> {
539 let manager_mutex = get_gui_manager()?;
540 let manager_guard = manager_mutex
541 .lock()
542 .map_err(|_| GuiOperationResult::Error {
543 message: "Failed to acquire GUI manager lock".to_string(),
544 error_code: GuiErrorCode::ThreadCommunicationFailed,
545 recoverable: false,
546 })?;
547
548 match manager_guard.as_ref() {
549 Some(manager) => manager.health_check(),
550 None => Err(GuiOperationResult::Error {
551 message: "GUI manager not initialized".to_string(),
552 error_code: GuiErrorCode::InvalidState,
553 recoverable: true,
554 }),
555 }
556}