Skip to main content

runmat_plot/gui/
lifecycle.rs

1//! Figure lifecycle helpers for native GUI windows.
2//!
3//! This module lets embedding runtimes associate RunMat figure handles with
4//! window instances so that lifecycle events (e.g. MATLAB's `close`) can
5//! gracefully tear down the corresponding OS windows without polling.
6
7use 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
34/// Render a figure in an interactive window that is tied to a specific MATLAB
35/// figure handle.
36///
37/// When the runtime later emits a `FigureEventKind::Closed` for the same handle,
38/// calling [`request_close`] will trigger the associated window to shut down.
39pub 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    if !figure.visible {
84        request_close(handle);
85        return Ok(format!("Figure {handle} is hidden"));
86    }
87
88    let registration = register_handle(handle)?;
89    let signal = registration.signal();
90    let window_title = figure.window_title(Some(handle));
91    let mut last_err: Option<String> = None;
92
93    for backend in backend_preference() {
94        let fig_clone = figure.clone();
95        let sig_clone = clone_signal(&signal);
96        let title_clone = window_title.clone();
97        let attempt = match backend {
98            DesktopBackend::NativeWindow => render_via_native(fig_clone, sig_clone, title_clone),
99            DesktopBackend::GuiThread => render_via_gui_thread(fig_clone, sig_clone, title_clone),
100            DesktopBackend::SingleWindow => {
101                render_via_single_window(fig_clone, sig_clone, title_clone)
102            }
103        };
104
105        match attempt {
106            Ok(msg) => return Ok(msg),
107            Err(err) => {
108                warn!("runmat-plot: backend {:?} failed: {}", backend, err);
109                last_err = Some(err);
110            }
111        }
112    }
113
114    Err(last_err.unwrap_or_else(|| "No interactive plotting backend succeeded".to_string()))
115}
116
117/// Request that the window associated with `handle` close.
118pub fn request_close(handle: u32) {
119    if let Ok(map) = WINDOW_SIGNALS.lock() {
120        if let Some(signal) = map.get(&handle) {
121            signal.store(true, Ordering::SeqCst);
122        }
123    }
124}
125
126fn backend_preference() -> &'static [DesktopBackend] {
127    BACKEND_PREF
128        .get_or_init(|| parse_backend_env().unwrap_or_else(default_backend_order))
129        .as_slice()
130}
131
132fn parse_backend_env() -> Option<Vec<DesktopBackend>> {
133    let raw = env::var("RUNMAT_PLOT_DESKTOP_BACKEND").ok()?;
134    let mut list = Vec::new();
135    for token in raw.split(',') {
136        let trimmed = token.trim().to_ascii_lowercase();
137        if trimmed.is_empty() {
138            continue;
139        }
140        let backend = match trimmed.as_str() {
141            "native" | "native_window" => DesktopBackend::NativeWindow,
142            "gui" | "gui_thread" => DesktopBackend::GuiThread,
143            "single" | "single_window" => DesktopBackend::SingleWindow,
144            _ => continue,
145        };
146        list.push(backend);
147    }
148    if list.is_empty() {
149        None
150    } else {
151        Some(list)
152    }
153}
154
155fn default_backend_order() -> Vec<DesktopBackend> {
156    vec![
157        DesktopBackend::NativeWindow,
158        DesktopBackend::GuiThread,
159        DesktopBackend::SingleWindow,
160    ]
161}
162
163fn clone_signal(signal: &Option<CloseSignal>) -> Option<CloseSignal> {
164    signal.as_ref().map(Arc::clone)
165}
166
167fn render_via_native(
168    figure: Figure,
169    signal: Option<CloseSignal>,
170    window_title: String,
171) -> Result<String, String> {
172    crate::gui::initialize_native_window()
173        .map_err(|err| format!("native window init failed: {err}"))?;
174    crate::gui::show_plot_native_window_with_signal_and_title(figure, signal, Some(window_title))
175}
176
177fn render_via_gui_thread(
178    figure: Figure,
179    signal: Option<CloseSignal>,
180    window_title: String,
181) -> Result<String, String> {
182    crate::gui::initialize_gui_manager()
183        .map_err(|err| format!("GUI manager init failed: {err}"))?;
184    match crate::gui::show_plot_global_with_signal_and_title(figure, signal, Some(window_title)) {
185        Ok(result) => gui_result_to_string(result),
186        Err(err) => gui_result_to_string(err),
187    }
188}
189
190fn render_via_single_window(
191    figure: Figure,
192    signal: Option<CloseSignal>,
193    window_title: String,
194) -> Result<String, String> {
195    crate::gui::single_window_manager::show_plot_sequential_with_signal_and_title(
196        figure,
197        signal,
198        Some(window_title),
199    )
200}
201
202fn gui_result_to_string(result: crate::gui::GuiOperationResult) -> Result<String, String> {
203    match result {
204        crate::gui::GuiOperationResult::Success(msg)
205        | crate::gui::GuiOperationResult::Cancelled(msg) => Ok(msg),
206        crate::gui::GuiOperationResult::Error { message, .. } => Err(message),
207    }
208}