1use crate::gui::lifecycle::CloseSignal;
8use crate::plots::Figure;
9use runmat_time::Instant;
10use std::sync::{mpsc, Arc, Mutex, OnceLock};
11use std::thread::{self, ThreadId};
12
13#[derive(Debug)]
15pub enum GuiThreadMessage {
16 ShowPlot {
18 figure: Box<Figure>,
19 response: mpsc::Sender<GuiOperationResult>,
20 close_signal: Option<CloseSignal>,
21 },
22 CloseAll {
24 response: mpsc::Sender<GuiOperationResult>,
25 },
26 HealthCheck {
28 response: mpsc::Sender<GuiOperationResult>,
29 },
30 Shutdown,
32}
33
34#[derive(Debug, Clone)]
36pub enum GuiOperationResult {
37 Success(String),
39 Error {
41 message: String,
42 error_code: GuiErrorCode,
43 recoverable: bool,
44 },
45 Cancelled(String),
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum GuiErrorCode {
52 EventLoopCreationFailed,
54 WindowCreationFailed,
56 WgpuInitializationFailed,
58 ThreadCommunicationFailed,
60 MainThreadViolation,
62 ResourceExhaustion,
64 InvalidState,
66 PlatformError,
68 Unknown,
70}
71
72impl std::fmt::Display for GuiOperationResult {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 match self {
75 GuiOperationResult::Success(msg) => write!(f, "Success: {msg}"),
76 GuiOperationResult::Error {
77 message,
78 error_code,
79 recoverable,
80 } => {
81 write!(
82 f,
83 "Error [{error_code:?}]: {message} (recoverable: {recoverable})"
84 )
85 }
86 GuiOperationResult::Cancelled(msg) => write!(f, "Cancelled: {msg}"),
87 }
88 }
89}
90
91impl std::error::Error for GuiOperationResult {}
92
93pub struct MainThreadDetector {
95 main_thread_id: OnceLock<ThreadId>,
96}
97
98impl Default for MainThreadDetector {
99 fn default() -> Self {
100 Self::new()
101 }
102}
103
104impl MainThreadDetector {
105 pub const fn new() -> Self {
107 Self {
108 main_thread_id: OnceLock::new(),
109 }
110 }
111
112 pub fn register_main_thread(&self) {
115 let current_id = thread::current().id();
116 if self.main_thread_id.set(current_id).is_err() {
117 }
119 }
120
121 pub fn is_main_thread(&self) -> bool {
123 if let Some(main_id) = self.main_thread_id.get() {
124 return thread::current().id() == *main_id;
125 }
126
127 #[cfg(target_os = "macos")]
128 {
129 if is_macos_main_thread() {
130 self.register_main_thread();
131 true
132 } else {
133 false
134 }
135 }
136
137 #[cfg(not(target_os = "macos"))]
138 {
139 self.register_main_thread();
141 true
142 }
143 }
144
145 pub fn main_thread_id(&self) -> Option<ThreadId> {
147 self.main_thread_id.get().copied()
148 }
149}
150
151#[cfg(target_os = "macos")]
152fn is_macos_main_thread() -> bool {
153 unsafe { libc::pthread_main_np() != 0 }
154}
155
156static MAIN_THREAD_DETECTOR: MainThreadDetector = MainThreadDetector::new();
158
159pub fn register_main_thread() {
161 MAIN_THREAD_DETECTOR.register_main_thread();
162}
163
164pub fn is_main_thread() -> bool {
166 MAIN_THREAD_DETECTOR.is_main_thread()
167}
168
169pub struct GuiThreadManager {
171 sender: mpsc::Sender<GuiThreadMessage>,
173 thread_handle: Option<thread::JoinHandle<Result<(), GuiOperationResult>>>,
175 health_state: Arc<Mutex<GuiHealthState>>,
177}
178
179#[derive(Debug, Clone)]
181struct GuiHealthState {
182 last_response: Instant,
183 response_count: u64,
184 error_count: u64,
185 is_healthy: bool,
186}
187
188impl GuiThreadManager {
189 pub fn new() -> Result<Self, GuiOperationResult> {
193 #[cfg(target_os = "macos")]
195 if !is_main_thread() {
196 return Err(GuiOperationResult::Error {
197 message: "GuiThreadManager must be created on the main thread on macOS".to_string(),
198 error_code: GuiErrorCode::MainThreadViolation,
199 recoverable: false,
200 });
201 }
202
203 let (sender, receiver) = mpsc::channel();
204 let health_state = Arc::new(Mutex::new(GuiHealthState {
205 last_response: Instant::now(),
206 response_count: 0,
207 error_count: 0,
208 is_healthy: true,
209 }));
210
211 let health_state_clone = Arc::clone(&health_state);
212
213 let thread_handle = thread::Builder::new()
215 .name("runmat-gui".to_string())
216 .spawn(move || Self::gui_thread_main(receiver, health_state_clone))
217 .map_err(|e| GuiOperationResult::Error {
218 message: format!("Failed to spawn GUI thread: {e}"),
219 error_code: GuiErrorCode::ThreadCommunicationFailed,
220 recoverable: false,
221 })?;
222
223 Ok(Self {
224 sender,
225 thread_handle: Some(thread_handle),
226 health_state,
227 })
228 }
229
230 fn gui_thread_main(
232 receiver: mpsc::Receiver<GuiThreadMessage>,
233 health_state: Arc<Mutex<GuiHealthState>>,
234 ) -> Result<(), GuiOperationResult> {
235 log::info!("GUI thread started successfully");
236
237 #[cfg(feature = "gui")]
239 let gui_context = Self::initialize_gui_context()?;
240
241 loop {
242 match receiver.recv() {
243 Ok(message) => {
244 let result = Self::handle_gui_message(message, &gui_context);
245
246 if let Ok(mut health) = health_state.lock() {
248 health.last_response = Instant::now();
249 health.response_count += 1;
250
251 if let Some(GuiOperationResult::Error { .. }) = &result {
252 health.error_count += 1;
253 health.is_healthy = health.error_count < 10; }
255 }
256
257 if result.is_none() {
259 break;
260 }
261 }
262 Err(_) => {
263 log::info!("GUI thread channel closed, shutting down");
265 break;
266 }
267 }
268 }
269
270 log::info!("GUI thread exiting gracefully");
271 Ok(())
272 }
273
274 #[cfg(feature = "gui")]
276 fn initialize_gui_context() -> Result<GuiContext, GuiOperationResult> {
277 use crate::gui::window::WindowConfig;
278
279 Ok(GuiContext {
281 _default_config: WindowConfig::default(),
282 _active_windows: Vec::new(),
283 })
284 }
285
286 #[cfg(not(feature = "gui"))]
287 fn initialize_gui_context() -> Result<GuiContext, GuiOperationResult> {
288 Err(GuiOperationResult::Error {
289 message: "GUI feature not enabled".to_string(),
290 error_code: GuiErrorCode::InvalidState,
291 recoverable: false,
292 })
293 }
294
295 fn handle_gui_message(
297 message: GuiThreadMessage,
298 gui_context: &GuiContext,
299 ) -> Option<GuiOperationResult> {
300 match message {
301 GuiThreadMessage::ShowPlot {
302 figure,
303 response,
304 close_signal,
305 } => {
306 let result = Self::handle_show_plot(figure, close_signal, gui_context);
307 let _ = response.send(result.clone());
308 Some(result)
309 }
310 GuiThreadMessage::CloseAll { response } => {
311 let result = Self::handle_close_all(gui_context);
312 let _ = response.send(result.clone());
313 Some(result)
314 }
315 GuiThreadMessage::HealthCheck { response } => {
316 let result = GuiOperationResult::Success("GUI thread healthy".to_string());
317 let _ = response.send(result.clone());
318 Some(result)
319 }
320 GuiThreadMessage::Shutdown => {
321 log::info!("GUI thread received shutdown signal");
322 None }
324 }
325 }
326
327 #[cfg(feature = "gui")]
329 fn handle_show_plot(
330 figure: Box<Figure>,
331 close_signal: Option<CloseSignal>,
332 _gui_context: &GuiContext,
333 ) -> GuiOperationResult {
334 use crate::gui::{window::WindowConfig, PlotWindow};
335
336 let rt = match tokio::runtime::Runtime::new() {
338 Ok(rt) => rt,
339 Err(e) => {
340 return GuiOperationResult::Error {
341 message: format!("Failed to create async runtime: {e}"),
342 error_code: GuiErrorCode::ResourceExhaustion,
343 recoverable: true,
344 }
345 }
346 };
347
348 rt.block_on(async {
349 let config = WindowConfig::default();
350 let mut window = match PlotWindow::new(config).await {
351 Ok(window) => window,
352 Err(e) => {
353 return GuiOperationResult::Error {
354 message: format!("Failed to create window: {e}"),
355 error_code: GuiErrorCode::WindowCreationFailed,
356 recoverable: true,
357 }
358 }
359 };
360
361 if let Some(sig) = close_signal {
362 window.install_close_signal(sig);
363 }
364
365 window.set_figure(*figure);
367
368 match window.run().await {
370 Ok(_) => GuiOperationResult::Success("Plot window closed".to_string()),
371 Err(e) => GuiOperationResult::Error {
372 message: format!("Window runtime error: {e}"),
373 error_code: GuiErrorCode::PlatformError,
374 recoverable: true,
375 },
376 }
377 })
378 }
379
380 #[cfg(not(feature = "gui"))]
381 fn handle_show_plot(
382 _figure: Box<Figure>,
383 _close_signal: Option<CloseSignal>,
384 _gui_context: &GuiContext,
385 ) -> GuiOperationResult {
386 GuiOperationResult::Error {
387 message: "GUI feature not enabled".to_string(),
388 error_code: GuiErrorCode::InvalidState,
389 recoverable: false,
390 }
391 }
392
393 fn handle_close_all(_gui_context: &GuiContext) -> GuiOperationResult {
395 GuiOperationResult::Success("All windows closed".to_string())
397 }
398
399 pub fn show_plot(&self, figure: Figure) -> Result<GuiOperationResult, GuiOperationResult> {
401 self.show_plot_with_signal(figure, None)
402 }
403
404 pub fn show_plot_with_signal(
405 &self,
406 figure: Figure,
407 close_signal: Option<CloseSignal>,
408 ) -> Result<GuiOperationResult, GuiOperationResult> {
409 let (response_tx, response_rx) = mpsc::channel();
410
411 let message = GuiThreadMessage::ShowPlot {
412 figure: Box::new(figure),
413 response: response_tx,
414 close_signal,
415 };
416
417 self.sender
419 .send(message)
420 .map_err(|_| GuiOperationResult::Error {
421 message: "GUI thread is not responding".to_string(),
422 error_code: GuiErrorCode::ThreadCommunicationFailed,
423 recoverable: false,
424 })?;
425
426 match response_rx.recv_timeout(std::time::Duration::from_secs(30)) {
428 Ok(result) => Ok(result),
429 Err(mpsc::RecvTimeoutError::Timeout) => Err(GuiOperationResult::Cancelled(
430 "GUI operation timed out after 30 seconds".to_string(),
431 )),
432 Err(mpsc::RecvTimeoutError::Disconnected) => Err(GuiOperationResult::Error {
433 message: "GUI thread disconnected unexpectedly".to_string(),
434 error_code: GuiErrorCode::ThreadCommunicationFailed,
435 recoverable: false,
436 }),
437 }
438 }
439
440 pub fn health_check(&self) -> Result<GuiOperationResult, GuiOperationResult> {
442 let (response_tx, response_rx) = mpsc::channel();
443
444 let message = GuiThreadMessage::HealthCheck {
445 response: response_tx,
446 };
447
448 self.sender
449 .send(message)
450 .map_err(|_| GuiOperationResult::Error {
451 message: "GUI thread is not responding".to_string(),
452 error_code: GuiErrorCode::ThreadCommunicationFailed,
453 recoverable: false,
454 })?;
455
456 match response_rx.recv_timeout(std::time::Duration::from_secs(5)) {
457 Ok(result) => Ok(result),
458 Err(_) => Err(GuiOperationResult::Error {
459 message: "GUI thread health check failed".to_string(),
460 error_code: GuiErrorCode::ThreadCommunicationFailed,
461 recoverable: true,
462 }),
463 }
464 }
465
466 pub fn get_health_state(&self) -> Option<(u64, u64, bool)> {
468 self.health_state
469 .lock()
470 .ok()
471 .map(|health| (health.response_count, health.error_count, health.is_healthy))
472 }
473
474 pub fn shutdown(mut self) -> Result<(), GuiOperationResult> {
476 let _ = self.sender.send(GuiThreadMessage::Shutdown);
478
479 if let Some(handle) = self.thread_handle.take() {
481 match handle.join() {
482 Ok(Ok(())) => Ok(()),
483 Ok(Err(e)) => Err(e),
484 Err(_) => Err(GuiOperationResult::Error {
485 message: "GUI thread panicked during shutdown".to_string(),
486 error_code: GuiErrorCode::PlatformError,
487 recoverable: false,
488 }),
489 }
490 } else {
491 Ok(())
492 }
493 }
494}
495
496impl Drop for GuiThreadManager {
498 fn drop(&mut self) {
499 if self.thread_handle.is_some() {
500 log::warn!("GuiThreadManager dropped without explicit shutdown");
501 let _ = self.sender.send(GuiThreadMessage::Shutdown);
502
503 if let Some(handle) = self.thread_handle.take() {
505 let _ = handle.join();
506 }
507 }
508 }
509}
510
511#[cfg(feature = "gui")]
513struct GuiContext {
514 _default_config: crate::gui::window::WindowConfig,
515 _active_windows: Vec<String>, }
517
518#[cfg(not(feature = "gui"))]
519struct GuiContext {
520 }
522
523static GUI_MANAGER: OnceLock<Arc<Mutex<Option<GuiThreadManager>>>> = OnceLock::new();
525
526pub fn initialize_gui_manager() -> Result<(), GuiOperationResult> {
528 let manager_mutex = GUI_MANAGER.get_or_init(|| Arc::new(Mutex::new(None)));
529
530 let mut manager_guard = manager_mutex
531 .lock()
532 .map_err(|_| GuiOperationResult::Error {
533 message: "Failed to acquire GUI manager lock".to_string(),
534 error_code: GuiErrorCode::ThreadCommunicationFailed,
535 recoverable: false,
536 })?;
537
538 if manager_guard.is_some() {
539 return Ok(()); }
541
542 let manager = GuiThreadManager::new()?;
543 *manager_guard = Some(manager);
544
545 log::info!("Global GUI thread manager initialized successfully");
546 Ok(())
547}
548
549pub fn get_gui_manager() -> Result<Arc<Mutex<Option<GuiThreadManager>>>, GuiOperationResult> {
551 GUI_MANAGER
552 .get()
553 .ok_or_else(|| GuiOperationResult::Error {
554 message: "GUI manager not initialized. Call initialize_gui_manager() first."
555 .to_string(),
556 error_code: GuiErrorCode::InvalidState,
557 recoverable: true,
558 })
559 .map(Arc::clone)
560}
561
562pub fn show_plot_global(figure: Figure) -> Result<GuiOperationResult, GuiOperationResult> {
564 show_plot_global_with_signal(figure, None)
565}
566
567pub fn show_plot_global_with_signal(
568 figure: Figure,
569 close_signal: Option<CloseSignal>,
570) -> Result<GuiOperationResult, GuiOperationResult> {
571 let manager_mutex = get_gui_manager()?;
572 let manager_guard = manager_mutex
573 .lock()
574 .map_err(|_| GuiOperationResult::Error {
575 message: "Failed to acquire GUI manager lock".to_string(),
576 error_code: GuiErrorCode::ThreadCommunicationFailed,
577 recoverable: false,
578 })?;
579
580 match manager_guard.as_ref() {
581 Some(manager) => manager.show_plot_with_signal(figure, close_signal),
582 None => Err(GuiOperationResult::Error {
583 message: "GUI manager not initialized".to_string(),
584 error_code: GuiErrorCode::InvalidState,
585 recoverable: true,
586 }),
587 }
588}
589
590pub fn health_check_global() -> Result<GuiOperationResult, GuiOperationResult> {
592 let manager_mutex = get_gui_manager()?;
593 let manager_guard = manager_mutex
594 .lock()
595 .map_err(|_| GuiOperationResult::Error {
596 message: "Failed to acquire GUI manager lock".to_string(),
597 error_code: GuiErrorCode::ThreadCommunicationFailed,
598 recoverable: false,
599 })?;
600
601 match manager_guard.as_ref() {
602 Some(manager) => manager.health_check(),
603 None => Err(GuiOperationResult::Error {
604 message: "GUI manager not initialized".to_string(),
605 error_code: GuiErrorCode::InvalidState,
606 recoverable: true,
607 }),
608 }
609}