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 window_title: Option<String>,
22 },
23 CloseAll {
25 response: mpsc::Sender<GuiOperationResult>,
26 },
27 HealthCheck {
29 response: mpsc::Sender<GuiOperationResult>,
30 },
31 Shutdown,
33}
34
35#[derive(Debug, Clone)]
37pub enum GuiOperationResult {
38 Success(String),
40 Error {
42 message: String,
43 error_code: GuiErrorCode,
44 recoverable: bool,
45 },
46 Cancelled(String),
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum GuiErrorCode {
53 EventLoopCreationFailed,
55 WindowCreationFailed,
57 WgpuInitializationFailed,
59 ThreadCommunicationFailed,
61 MainThreadViolation,
63 ResourceExhaustion,
65 InvalidState,
67 PlatformError,
69 Unknown,
71}
72
73impl std::fmt::Display for GuiOperationResult {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 match self {
76 GuiOperationResult::Success(msg) => write!(f, "Success: {msg}"),
77 GuiOperationResult::Error {
78 message,
79 error_code,
80 recoverable,
81 } => {
82 write!(
83 f,
84 "Error [{error_code:?}]: {message} (recoverable: {recoverable})"
85 )
86 }
87 GuiOperationResult::Cancelled(msg) => write!(f, "Cancelled: {msg}"),
88 }
89 }
90}
91
92impl std::error::Error for GuiOperationResult {}
93
94pub struct MainThreadDetector {
96 main_thread_id: OnceLock<ThreadId>,
97}
98
99impl Default for MainThreadDetector {
100 fn default() -> Self {
101 Self::new()
102 }
103}
104
105impl MainThreadDetector {
106 pub const fn new() -> Self {
108 Self {
109 main_thread_id: OnceLock::new(),
110 }
111 }
112
113 pub fn register_main_thread(&self) {
116 let current_id = thread::current().id();
117 if self.main_thread_id.set(current_id).is_err() {
118 }
120 }
121
122 pub fn is_main_thread(&self) -> bool {
124 if let Some(main_id) = self.main_thread_id.get() {
125 return thread::current().id() == *main_id;
126 }
127
128 #[cfg(target_os = "macos")]
129 {
130 if is_macos_main_thread() {
131 self.register_main_thread();
132 true
133 } else {
134 false
135 }
136 }
137
138 #[cfg(not(target_os = "macos"))]
139 {
140 self.register_main_thread();
142 true
143 }
144 }
145
146 pub fn main_thread_id(&self) -> Option<ThreadId> {
148 self.main_thread_id.get().copied()
149 }
150}
151
152#[cfg(target_os = "macos")]
153fn is_macos_main_thread() -> bool {
154 unsafe { libc::pthread_main_np() != 0 }
155}
156
157static MAIN_THREAD_DETECTOR: MainThreadDetector = MainThreadDetector::new();
159
160pub fn register_main_thread() {
162 MAIN_THREAD_DETECTOR.register_main_thread();
163}
164
165pub fn is_main_thread() -> bool {
167 MAIN_THREAD_DETECTOR.is_main_thread()
168}
169
170pub struct GuiThreadManager {
172 sender: mpsc::Sender<GuiThreadMessage>,
174 thread_handle: Option<thread::JoinHandle<Result<(), GuiOperationResult>>>,
176 health_state: Arc<Mutex<GuiHealthState>>,
178}
179
180#[derive(Debug, Clone)]
182struct GuiHealthState {
183 last_response: Instant,
184 response_count: u64,
185 error_count: u64,
186 is_healthy: bool,
187}
188
189impl GuiThreadManager {
190 pub fn new() -> Result<Self, GuiOperationResult> {
194 #[cfg(target_os = "macos")]
196 if !is_main_thread() {
197 return Err(GuiOperationResult::Error {
198 message: "GuiThreadManager must be created on the main thread on macOS".to_string(),
199 error_code: GuiErrorCode::MainThreadViolation,
200 recoverable: false,
201 });
202 }
203
204 let (sender, receiver) = mpsc::channel();
205 let health_state = Arc::new(Mutex::new(GuiHealthState {
206 last_response: Instant::now(),
207 response_count: 0,
208 error_count: 0,
209 is_healthy: true,
210 }));
211
212 let health_state_clone = Arc::clone(&health_state);
213
214 let thread_handle = thread::Builder::new()
216 .name("runmat-gui".to_string())
217 .spawn(move || Self::gui_thread_main(receiver, health_state_clone))
218 .map_err(|e| GuiOperationResult::Error {
219 message: format!("Failed to spawn GUI thread: {e}"),
220 error_code: GuiErrorCode::ThreadCommunicationFailed,
221 recoverable: false,
222 })?;
223
224 Ok(Self {
225 sender,
226 thread_handle: Some(thread_handle),
227 health_state,
228 })
229 }
230
231 fn gui_thread_main(
233 receiver: mpsc::Receiver<GuiThreadMessage>,
234 health_state: Arc<Mutex<GuiHealthState>>,
235 ) -> Result<(), GuiOperationResult> {
236 log::info!("GUI thread started successfully");
237
238 #[cfg(feature = "gui")]
240 let gui_context = Self::initialize_gui_context()?;
241
242 loop {
243 match receiver.recv() {
244 Ok(message) => {
245 let result = Self::handle_gui_message(message, &gui_context);
246
247 if let Ok(mut health) = health_state.lock() {
249 health.last_response = Instant::now();
250 health.response_count += 1;
251
252 if let Some(GuiOperationResult::Error { .. }) = &result {
253 health.error_count += 1;
254 health.is_healthy = health.error_count < 10; }
256 }
257
258 if result.is_none() {
260 break;
261 }
262 }
263 Err(_) => {
264 log::info!("GUI thread channel closed, shutting down");
266 break;
267 }
268 }
269 }
270
271 log::info!("GUI thread exiting gracefully");
272 Ok(())
273 }
274
275 #[cfg(feature = "gui")]
277 fn initialize_gui_context() -> Result<GuiContext, GuiOperationResult> {
278 use crate::gui::window::WindowConfig;
279
280 Ok(GuiContext {
282 _default_config: WindowConfig::default(),
283 _active_windows: Vec::new(),
284 })
285 }
286
287 #[cfg(not(feature = "gui"))]
288 fn initialize_gui_context() -> Result<GuiContext, GuiOperationResult> {
289 Err(GuiOperationResult::Error {
290 message: "GUI feature not enabled".to_string(),
291 error_code: GuiErrorCode::InvalidState,
292 recoverable: false,
293 })
294 }
295
296 fn handle_gui_message(
298 message: GuiThreadMessage,
299 gui_context: &GuiContext,
300 ) -> Option<GuiOperationResult> {
301 match message {
302 GuiThreadMessage::ShowPlot {
303 figure,
304 response,
305 close_signal,
306 window_title,
307 } => {
308 let result =
309 Self::handle_show_plot(figure, close_signal, window_title, gui_context);
310 let _ = response.send(result.clone());
311 Some(result)
312 }
313 GuiThreadMessage::CloseAll { response } => {
314 let result = Self::handle_close_all(gui_context);
315 let _ = response.send(result.clone());
316 Some(result)
317 }
318 GuiThreadMessage::HealthCheck { response } => {
319 let result = GuiOperationResult::Success("GUI thread healthy".to_string());
320 let _ = response.send(result.clone());
321 Some(result)
322 }
323 GuiThreadMessage::Shutdown => {
324 log::info!("GUI thread received shutdown signal");
325 None }
327 }
328 }
329
330 #[cfg(feature = "gui")]
332 fn handle_show_plot(
333 figure: Box<Figure>,
334 close_signal: Option<CloseSignal>,
335 window_title: Option<String>,
336 _gui_context: &GuiContext,
337 ) -> GuiOperationResult {
338 use crate::gui::{window::WindowConfig, PlotWindow};
339
340 let rt = match tokio::runtime::Runtime::new() {
342 Ok(rt) => rt,
343 Err(e) => {
344 return GuiOperationResult::Error {
345 message: format!("Failed to create async runtime: {e}"),
346 error_code: GuiErrorCode::ResourceExhaustion,
347 recoverable: true,
348 }
349 }
350 };
351
352 rt.block_on(async {
353 let mut config = WindowConfig::default();
354 if let Some(title) = window_title {
355 config.title = title;
356 }
357 let mut window = match PlotWindow::new(config).await {
358 Ok(window) => window,
359 Err(e) => {
360 return GuiOperationResult::Error {
361 message: format!("Failed to create window: {e}"),
362 error_code: GuiErrorCode::WindowCreationFailed,
363 recoverable: true,
364 }
365 }
366 };
367
368 if let Some(sig) = close_signal {
369 window.install_close_signal(sig);
370 }
371
372 window.set_figure(*figure);
374
375 match window.run().await {
377 Ok(_) => GuiOperationResult::Success("Plot window closed".to_string()),
378 Err(e) => GuiOperationResult::Error {
379 message: format!("Window runtime error: {e}"),
380 error_code: GuiErrorCode::PlatformError,
381 recoverable: true,
382 },
383 }
384 })
385 }
386
387 #[cfg(not(feature = "gui"))]
388 fn handle_show_plot(
389 _figure: Box<Figure>,
390 _close_signal: Option<CloseSignal>,
391 _gui_context: &GuiContext,
392 ) -> GuiOperationResult {
393 GuiOperationResult::Error {
394 message: "GUI feature not enabled".to_string(),
395 error_code: GuiErrorCode::InvalidState,
396 recoverable: false,
397 }
398 }
399
400 fn handle_close_all(_gui_context: &GuiContext) -> GuiOperationResult {
402 GuiOperationResult::Success("All windows closed".to_string())
404 }
405
406 pub fn show_plot(&self, figure: Figure) -> Result<GuiOperationResult, GuiOperationResult> {
408 self.show_plot_with_signal(figure, None)
409 }
410
411 pub fn show_plot_with_signal(
412 &self,
413 figure: Figure,
414 close_signal: Option<CloseSignal>,
415 ) -> Result<GuiOperationResult, GuiOperationResult> {
416 self.show_plot_with_signal_and_title(figure, close_signal, None)
417 }
418
419 pub fn show_plot_with_signal_and_title(
420 &self,
421 figure: Figure,
422 close_signal: Option<CloseSignal>,
423 window_title: Option<String>,
424 ) -> Result<GuiOperationResult, GuiOperationResult> {
425 let (response_tx, response_rx) = mpsc::channel();
426
427 let message = GuiThreadMessage::ShowPlot {
428 figure: Box::new(figure),
429 response: response_tx,
430 close_signal,
431 window_title,
432 };
433
434 self.sender
436 .send(message)
437 .map_err(|_| GuiOperationResult::Error {
438 message: "GUI thread is not responding".to_string(),
439 error_code: GuiErrorCode::ThreadCommunicationFailed,
440 recoverable: false,
441 })?;
442
443 match response_rx.recv_timeout(std::time::Duration::from_secs(30)) {
445 Ok(result) => Ok(result),
446 Err(mpsc::RecvTimeoutError::Timeout) => Err(GuiOperationResult::Cancelled(
447 "GUI operation timed out after 30 seconds".to_string(),
448 )),
449 Err(mpsc::RecvTimeoutError::Disconnected) => Err(GuiOperationResult::Error {
450 message: "GUI thread disconnected unexpectedly".to_string(),
451 error_code: GuiErrorCode::ThreadCommunicationFailed,
452 recoverable: false,
453 }),
454 }
455 }
456
457 pub fn health_check(&self) -> Result<GuiOperationResult, GuiOperationResult> {
459 let (response_tx, response_rx) = mpsc::channel();
460
461 let message = GuiThreadMessage::HealthCheck {
462 response: response_tx,
463 };
464
465 self.sender
466 .send(message)
467 .map_err(|_| GuiOperationResult::Error {
468 message: "GUI thread is not responding".to_string(),
469 error_code: GuiErrorCode::ThreadCommunicationFailed,
470 recoverable: false,
471 })?;
472
473 match response_rx.recv_timeout(std::time::Duration::from_secs(5)) {
474 Ok(result) => Ok(result),
475 Err(_) => Err(GuiOperationResult::Error {
476 message: "GUI thread health check failed".to_string(),
477 error_code: GuiErrorCode::ThreadCommunicationFailed,
478 recoverable: true,
479 }),
480 }
481 }
482
483 pub fn get_health_state(&self) -> Option<(u64, u64, bool)> {
485 self.health_state
486 .lock()
487 .ok()
488 .map(|health| (health.response_count, health.error_count, health.is_healthy))
489 }
490
491 pub fn shutdown(mut self) -> Result<(), GuiOperationResult> {
493 let _ = self.sender.send(GuiThreadMessage::Shutdown);
495
496 if let Some(handle) = self.thread_handle.take() {
498 match handle.join() {
499 Ok(Ok(())) => Ok(()),
500 Ok(Err(e)) => Err(e),
501 Err(_) => Err(GuiOperationResult::Error {
502 message: "GUI thread panicked during shutdown".to_string(),
503 error_code: GuiErrorCode::PlatformError,
504 recoverable: false,
505 }),
506 }
507 } else {
508 Ok(())
509 }
510 }
511}
512
513impl Drop for GuiThreadManager {
515 fn drop(&mut self) {
516 if self.thread_handle.is_some() {
517 log::warn!("GuiThreadManager dropped without explicit shutdown");
518 let _ = self.sender.send(GuiThreadMessage::Shutdown);
519
520 if let Some(handle) = self.thread_handle.take() {
522 let _ = handle.join();
523 }
524 }
525 }
526}
527
528#[cfg(feature = "gui")]
530struct GuiContext {
531 _default_config: crate::gui::window::WindowConfig,
532 _active_windows: Vec<String>, }
534
535#[cfg(not(feature = "gui"))]
536struct GuiContext {
537 }
539
540static GUI_MANAGER: OnceLock<Arc<Mutex<Option<GuiThreadManager>>>> = OnceLock::new();
542
543pub fn initialize_gui_manager() -> Result<(), GuiOperationResult> {
545 let manager_mutex = GUI_MANAGER.get_or_init(|| Arc::new(Mutex::new(None)));
546
547 let mut manager_guard = manager_mutex
548 .lock()
549 .map_err(|_| GuiOperationResult::Error {
550 message: "Failed to acquire GUI manager lock".to_string(),
551 error_code: GuiErrorCode::ThreadCommunicationFailed,
552 recoverable: false,
553 })?;
554
555 if manager_guard.is_some() {
556 return Ok(()); }
558
559 let manager = GuiThreadManager::new()?;
560 *manager_guard = Some(manager);
561
562 log::info!("Global GUI thread manager initialized successfully");
563 Ok(())
564}
565
566pub fn get_gui_manager() -> Result<Arc<Mutex<Option<GuiThreadManager>>>, GuiOperationResult> {
568 GUI_MANAGER
569 .get()
570 .ok_or_else(|| GuiOperationResult::Error {
571 message: "GUI manager not initialized. Call initialize_gui_manager() first."
572 .to_string(),
573 error_code: GuiErrorCode::InvalidState,
574 recoverable: true,
575 })
576 .map(Arc::clone)
577}
578
579pub fn show_plot_global(figure: Figure) -> Result<GuiOperationResult, GuiOperationResult> {
581 show_plot_global_with_signal(figure, None)
582}
583
584pub fn show_plot_global_with_signal(
585 figure: Figure,
586 close_signal: Option<CloseSignal>,
587) -> Result<GuiOperationResult, GuiOperationResult> {
588 show_plot_global_with_signal_and_title(figure, close_signal, None)
589}
590
591pub fn show_plot_global_with_signal_and_title(
592 figure: Figure,
593 close_signal: Option<CloseSignal>,
594 window_title: Option<String>,
595) -> Result<GuiOperationResult, GuiOperationResult> {
596 let manager_mutex = get_gui_manager()?;
597 let manager_guard = manager_mutex
598 .lock()
599 .map_err(|_| GuiOperationResult::Error {
600 message: "Failed to acquire GUI manager lock".to_string(),
601 error_code: GuiErrorCode::ThreadCommunicationFailed,
602 recoverable: false,
603 })?;
604
605 match manager_guard.as_ref() {
606 Some(manager) => {
607 manager.show_plot_with_signal_and_title(figure, close_signal, window_title)
608 }
609 None => Err(GuiOperationResult::Error {
610 message: "GUI manager not initialized".to_string(),
611 error_code: GuiErrorCode::InvalidState,
612 recoverable: true,
613 }),
614 }
615}
616
617pub fn health_check_global() -> Result<GuiOperationResult, GuiOperationResult> {
619 let manager_mutex = get_gui_manager()?;
620 let manager_guard = manager_mutex
621 .lock()
622 .map_err(|_| GuiOperationResult::Error {
623 message: "Failed to acquire GUI manager lock".to_string(),
624 error_code: GuiErrorCode::ThreadCommunicationFailed,
625 recoverable: false,
626 })?;
627
628 match manager_guard.as_ref() {
629 Some(manager) => manager.health_check(),
630 None => Err(GuiOperationResult::Error {
631 message: "GUI manager not initialized".to_string(),
632 error_code: GuiErrorCode::InvalidState,
633 recoverable: true,
634 }),
635 }
636}