runmat_plot/gui/
lifecycle.rs1use crate::plots::Figure;
8use log::warn;
9use once_cell::sync::{Lazy, OnceCell};
10use std::collections::HashMap;
11use std::env;
12use std::sync::atomic::{AtomicBool, Ordering};
13use std::sync::{Arc, Mutex};
14
15pub type CloseSignal = Arc<AtomicBool>;
16
17pub struct WindowRegistration {
18 handle: u32,
19 signal: Option<CloseSignal>,
20}
21
22static WINDOW_SIGNALS: Lazy<Mutex<HashMap<u32, CloseSignal>>> =
23 Lazy::new(|| Mutex::new(HashMap::new()));
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26enum DesktopBackend {
27 NativeWindow,
28 GuiThread,
29 SingleWindow,
30}
31
32static BACKEND_PREF: OnceCell<Vec<DesktopBackend>> = OnceCell::new();
33
34pub fn register_handle(handle: u32) -> Result<WindowRegistration, String> {
40 if handle == 0 {
41 return Ok(WindowRegistration {
42 handle,
43 signal: None,
44 });
45 }
46
47 let signal = Arc::new(AtomicBool::new(false));
48 {
49 let mut map = WINDOW_SIGNALS
50 .lock()
51 .map_err(|_| "failed to track plot window".to_string())?;
52 map.insert(handle, signal.clone());
53 }
54
55 Ok(WindowRegistration {
56 handle,
57 signal: Some(signal),
58 })
59}
60
61impl WindowRegistration {
62 pub fn signal(&self) -> Option<CloseSignal> {
63 self.signal.as_ref().map(Arc::clone)
64 }
65}
66
67impl Drop for WindowRegistration {
68 fn drop(&mut self) {
69 if self.handle == 0 {
70 return;
71 }
72 if let Ok(mut map) = WINDOW_SIGNALS.lock() {
73 map.remove(&self.handle);
74 }
75 }
76}
77
78pub fn render_figure(handle: u32, figure: Figure) -> Result<String, String> {
79 if handle == 0 {
80 return crate::show_interactive_platform_optimal(figure);
81 }
82
83 let registration = register_handle(handle)?;
84 let signal = registration.signal();
85 let mut last_err: Option<String> = None;
86
87 for backend in backend_preference() {
88 let fig_clone = figure.clone();
89 let sig_clone = clone_signal(&signal);
90 let attempt = match backend {
91 DesktopBackend::NativeWindow => render_via_native(fig_clone, sig_clone),
92 DesktopBackend::GuiThread => render_via_gui_thread(fig_clone, sig_clone),
93 DesktopBackend::SingleWindow => render_via_single_window(fig_clone, sig_clone),
94 };
95
96 match attempt {
97 Ok(msg) => return Ok(msg),
98 Err(err) => {
99 warn!("runmat-plot: backend {:?} failed: {}", backend, err);
100 last_err = Some(err);
101 }
102 }
103 }
104
105 Err(last_err.unwrap_or_else(|| "No interactive plotting backend succeeded".to_string()))
106}
107
108pub fn request_close(handle: u32) {
110 if let Ok(map) = WINDOW_SIGNALS.lock() {
111 if let Some(signal) = map.get(&handle) {
112 signal.store(true, Ordering::SeqCst);
113 }
114 }
115}
116
117fn backend_preference() -> &'static [DesktopBackend] {
118 BACKEND_PREF
119 .get_or_init(|| parse_backend_env().unwrap_or_else(default_backend_order))
120 .as_slice()
121}
122
123fn parse_backend_env() -> Option<Vec<DesktopBackend>> {
124 let raw = env::var("RUNMAT_PLOT_DESKTOP_BACKEND").ok()?;
125 let mut list = Vec::new();
126 for token in raw.split(',') {
127 let trimmed = token.trim().to_ascii_lowercase();
128 if trimmed.is_empty() {
129 continue;
130 }
131 let backend = match trimmed.as_str() {
132 "native" | "native_window" => DesktopBackend::NativeWindow,
133 "gui" | "gui_thread" => DesktopBackend::GuiThread,
134 "single" | "single_window" => DesktopBackend::SingleWindow,
135 _ => continue,
136 };
137 list.push(backend);
138 }
139 if list.is_empty() {
140 None
141 } else {
142 Some(list)
143 }
144}
145
146fn default_backend_order() -> Vec<DesktopBackend> {
147 vec![
148 DesktopBackend::NativeWindow,
149 DesktopBackend::GuiThread,
150 DesktopBackend::SingleWindow,
151 ]
152}
153
154fn clone_signal(signal: &Option<CloseSignal>) -> Option<CloseSignal> {
155 signal.as_ref().map(Arc::clone)
156}
157
158fn render_via_native(figure: Figure, signal: Option<CloseSignal>) -> Result<String, String> {
159 crate::gui::initialize_native_window()
160 .map_err(|err| format!("native window init failed: {err}"))?;
161 crate::gui::show_plot_native_window_with_signal(figure, signal)
162}
163
164fn render_via_gui_thread(figure: Figure, signal: Option<CloseSignal>) -> Result<String, String> {
165 crate::gui::initialize_gui_manager()
166 .map_err(|err| format!("GUI manager init failed: {err}"))?;
167 match crate::gui::show_plot_global_with_signal(figure, signal) {
168 Ok(result) => gui_result_to_string(result),
169 Err(err) => gui_result_to_string(err),
170 }
171}
172
173fn render_via_single_window(figure: Figure, signal: Option<CloseSignal>) -> Result<String, String> {
174 crate::gui::single_window_manager::show_plot_sequential_with_signal(figure, signal)
175}
176
177fn gui_result_to_string(result: crate::gui::GuiOperationResult) -> Result<String, String> {
178 match result {
179 crate::gui::GuiOperationResult::Success(msg)
180 | crate::gui::GuiOperationResult::Cancelled(msg) => Ok(msg),
181 crate::gui::GuiOperationResult::Error { message, .. } => Err(message),
182 }
183}