Skip to main content

runmat_runtime/builtins/plotting/core/
state.rs

1use glam::Vec4;
2use once_cell::sync::OnceCell;
3use runmat_builtins::Tensor;
4use runmat_plot::plots::{
5    surface::ColorMap, surface::ShadingMode, AxesKind, Figure, LegendStyle, LineStyle, PlotElement,
6    TextStyle,
7};
8use runmat_thread_local::runmat_thread_local;
9use std::cell::RefCell;
10use std::collections::{hash_map::Entry, HashMap, HashSet};
11use std::ops::{Deref, DerefMut};
12#[cfg(not(target_arch = "wasm32"))]
13use std::sync::MutexGuard;
14use std::sync::{Arc, Mutex};
15use thiserror::Error;
16
17use super::common::{default_figure, ERR_PLOTTING_UNAVAILABLE};
18#[cfg(not(all(target_arch = "wasm32", feature = "plot-web")))]
19use super::engine::render_figure;
20use super::web::current_plot_theme_config;
21use super::{plotting_error, plotting_error_with_source};
22
23use crate::builtins::common::map_control_flow_with_builtin;
24use crate::{BuiltinResult, RuntimeError};
25
26type AxisLimitSnapshot = (Option<(f64, f64)>, Option<(f64, f64)>);
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
29pub struct FigureHandle(u32);
30
31impl FigureHandle {
32    pub fn as_u32(self) -> u32 {
33        self.0
34    }
35
36    fn next(self) -> FigureHandle {
37        FigureHandle(self.0 + 1)
38    }
39}
40
41impl From<u32> for FigureHandle {
42    fn from(value: u32) -> Self {
43        FigureHandle(value.max(1))
44    }
45}
46
47impl Default for FigureHandle {
48    fn default() -> Self {
49        FigureHandle(1)
50    }
51}
52
53const DEFAULT_LINE_STYLE_ORDER: [LineStyle; 1] = [LineStyle::Solid];
54
55#[derive(Clone)]
56struct LineStyleCycle {
57    order: Vec<LineStyle>,
58    cursor: usize,
59}
60
61impl Default for LineStyleCycle {
62    fn default() -> Self {
63        Self {
64            order: DEFAULT_LINE_STYLE_ORDER.to_vec(),
65            cursor: 0,
66        }
67    }
68}
69
70impl LineStyleCycle {
71    fn next(&mut self) -> LineStyle {
72        if self.order.is_empty() {
73            self.order = DEFAULT_LINE_STYLE_ORDER.to_vec();
74        }
75        let style = self.order[self.cursor % self.order.len()];
76        self.cursor = (self.cursor + 1) % self.order.len();
77        style
78    }
79
80    fn set_order(&mut self, order: &[LineStyle]) {
81        if order.is_empty() {
82            self.order = DEFAULT_LINE_STYLE_ORDER.to_vec();
83        } else {
84            self.order = order.to_vec();
85        }
86        self.cursor = 0;
87    }
88
89    fn reset_cursor(&mut self) {
90        self.cursor = 0;
91    }
92}
93
94#[derive(Clone, Default)]
95struct LineColorCycle {
96    cursor: usize,
97}
98
99impl LineColorCycle {
100    fn next(&mut self) -> Vec4 {
101        let color = line_color_for_series_index(self.cursor);
102        self.cursor = self.cursor.saturating_add(1);
103        color
104    }
105
106    fn reset_cursor(&mut self) {
107        self.cursor = 0;
108    }
109}
110
111#[derive(Default)]
112struct FigureState {
113    figure: Figure,
114    active_axes: usize,
115    hold_per_axes: HashMap<usize, bool>,
116    line_style_cycles: HashMap<usize, LineStyleCycle>,
117    line_color_cycles: HashMap<usize, LineColorCycle>,
118    revision: u64,
119}
120
121impl FigureState {
122    fn new(handle: FigureHandle) -> Self {
123        let title = format!("Figure {}", handle.as_u32());
124        let figure = default_figure(&title, "X", "Y");
125        Self {
126            figure,
127            active_axes: 0,
128            hold_per_axes: HashMap::new(),
129            line_style_cycles: HashMap::new(),
130            line_color_cycles: HashMap::new(),
131            revision: 0,
132        }
133    }
134
135    fn hold(&self) -> bool {
136        *self.hold_per_axes.get(&self.active_axes).unwrap_or(&false)
137    }
138
139    fn set_hold(&mut self, hold: bool) {
140        self.hold_per_axes.insert(self.active_axes, hold);
141    }
142
143    fn cycle_for_axes_mut(&mut self, axes_index: usize) -> &mut LineStyleCycle {
144        self.line_style_cycles.entry(axes_index).or_default()
145    }
146
147    fn color_cycle_for_axes_mut(&mut self, axes_index: usize) -> &mut LineColorCycle {
148        self.line_color_cycles.entry(axes_index).or_default()
149    }
150
151    fn reset_cycle(&mut self, axes_index: usize) {
152        if let Some(cycle) = self.line_style_cycles.get_mut(&axes_index) {
153            cycle.reset_cursor();
154        }
155        if let Some(cycle) = self.line_color_cycles.get_mut(&axes_index) {
156            cycle.reset_cursor();
157        }
158    }
159}
160
161struct ActiveAxesContext {
162    axes_index: usize,
163    style_cycle_ptr: *mut LineStyleCycle,
164    color_cycle_ptr: *mut LineColorCycle,
165}
166
167struct AxesContextGuard {
168    _private: (),
169}
170
171impl AxesContextGuard {
172    fn install(state: &mut FigureState, axes_index: usize) -> Self {
173        let style_cycle_ptr = state.cycle_for_axes_mut(axes_index) as *mut LineStyleCycle;
174        let color_cycle_ptr = state.color_cycle_for_axes_mut(axes_index) as *mut LineColorCycle;
175        ACTIVE_AXES_CONTEXT.with(|ctx| {
176            debug_assert!(
177                ctx.borrow().is_none(),
178                "plot axes context already installed"
179            );
180            ctx.borrow_mut().replace(ActiveAxesContext {
181                axes_index,
182                style_cycle_ptr,
183                color_cycle_ptr,
184            });
185        });
186        Self { _private: () }
187    }
188}
189
190impl Drop for AxesContextGuard {
191    fn drop(&mut self) {
192        ACTIVE_AXES_CONTEXT.with(|ctx| {
193            ctx.borrow_mut().take();
194        });
195    }
196}
197
198fn with_active_style_cycle<R>(
199    axes_index: usize,
200    f: impl FnOnce(&mut LineStyleCycle) -> R,
201) -> Option<R> {
202    ACTIVE_AXES_CONTEXT.with(|ctx| {
203        let guard = ctx.borrow();
204        let active = guard.as_ref()?;
205        if active.axes_index != axes_index {
206            return None;
207        }
208        let cycle = unsafe { &mut *active.style_cycle_ptr };
209        Some(f(cycle))
210    })
211}
212
213fn with_active_color_cycle<R>(
214    axes_index: usize,
215    f: impl FnOnce(&mut LineColorCycle) -> R,
216) -> Option<R> {
217    ACTIVE_AXES_CONTEXT.with(|ctx| {
218        let guard = ctx.borrow();
219        let active = guard.as_ref()?;
220        if active.axes_index != axes_index {
221            return None;
222        }
223        let cycle = unsafe { &mut *active.color_cycle_ptr };
224        Some(f(cycle))
225    })
226}
227
228struct PlotRegistry {
229    current: FigureHandle,
230    next_handle: FigureHandle,
231    figures: HashMap<FigureHandle, FigureState>,
232    next_plot_child_handle: u64,
233    plot_children: HashMap<u64, PlotChildHandleState>,
234}
235
236#[derive(Clone, Debug)]
237pub struct HistogramHandleState {
238    pub figure: FigureHandle,
239    pub axes_index: usize,
240    pub plot_index: usize,
241    pub bin_edges: Vec<f64>,
242    pub raw_counts: Vec<f64>,
243    pub normalization: String,
244    pub display_name: Option<String>,
245}
246
247#[derive(Clone, Debug)]
248pub struct StemHandleState {
249    pub figure: FigureHandle,
250    pub axes_index: usize,
251    pub plot_index: usize,
252}
253
254#[derive(Clone, Debug)]
255pub struct SimplePlotHandleState {
256    pub figure: FigureHandle,
257    pub axes_index: usize,
258    pub plot_index: usize,
259}
260
261#[derive(Clone, Debug)]
262pub struct ErrorBarHandleState {
263    pub figure: FigureHandle,
264    pub axes_index: usize,
265    pub plot_index: usize,
266}
267
268#[derive(Clone, Debug)]
269pub struct QuiverHandleState {
270    pub figure: FigureHandle,
271    pub axes_index: usize,
272    pub plot_index: usize,
273}
274
275#[derive(Clone, Debug)]
276pub struct ImageHandleState {
277    pub figure: FigureHandle,
278    pub axes_index: usize,
279    pub plot_index: usize,
280}
281
282#[derive(Clone, Debug)]
283pub struct HeatmapHandleState {
284    pub figure: FigureHandle,
285    pub axes_index: usize,
286    pub plot_index: usize,
287    pub x_labels: Vec<String>,
288    pub y_labels: Vec<String>,
289    pub color_data: Tensor,
290}
291
292#[derive(Clone, Debug)]
293pub struct AreaHandleState {
294    pub figure: FigureHandle,
295    pub axes_index: usize,
296    pub plot_index: usize,
297}
298
299#[derive(Clone, Debug)]
300pub struct TextAnnotationHandleState {
301    pub figure: FigureHandle,
302    pub axes_index: usize,
303    pub annotation_index: usize,
304}
305
306#[derive(Clone, Debug)]
307pub enum PlotChildHandleState {
308    Histogram(HistogramHandleState),
309    Line(SimplePlotHandleState),
310    Scatter(SimplePlotHandleState),
311    Bar(SimplePlotHandleState),
312    Stem(StemHandleState),
313    ErrorBar(ErrorBarHandleState),
314    Stairs(SimplePlotHandleState),
315    Quiver(QuiverHandleState),
316    Image(ImageHandleState),
317    Heatmap(HeatmapHandleState),
318    Area(AreaHandleState),
319    Surface(SimplePlotHandleState),
320    Patch(SimplePlotHandleState),
321    Line3(SimplePlotHandleState),
322    Scatter3(SimplePlotHandleState),
323    Contour(SimplePlotHandleState),
324    ContourFill(SimplePlotHandleState),
325    ReferenceLine(SimplePlotHandleState),
326    Pie(SimplePlotHandleState),
327    Text(TextAnnotationHandleState),
328}
329
330impl Default for PlotRegistry {
331    fn default() -> Self {
332        Self {
333            current: FigureHandle::default(),
334            next_handle: FigureHandle::default().next(),
335            figures: HashMap::new(),
336            next_plot_child_handle: 1u64 << 40,
337            plot_children: HashMap::new(),
338        }
339    }
340}
341
342#[cfg(not(target_arch = "wasm32"))]
343static REGISTRY: OnceCell<Mutex<PlotRegistry>> = OnceCell::new();
344
345static TEST_PLOT_REGISTRY_LOCK: Mutex<()> = Mutex::new(());
346
347thread_local! {
348    static TEST_PLOT_OUTER_LOCK_HELD: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
349}
350
351#[doc(hidden)]
352pub struct PlotTestLockGuard {
353    _guard: std::sync::MutexGuard<'static, ()>,
354    disable_previous: Option<std::ffi::OsString>,
355    host_previous: Option<std::ffi::OsString>,
356}
357
358impl Drop for PlotTestLockGuard {
359    fn drop(&mut self) {
360        restore_env_var(
361            "RUNMAT_DISABLE_INTERACTIVE_PLOTS",
362            self.disable_previous.take(),
363        );
364        restore_env_var("RUNMAT_HOST_MANAGED_PLOTS", self.host_previous.take());
365        TEST_PLOT_OUTER_LOCK_HELD.with(|flag| flag.set(false));
366    }
367}
368
369impl PlotTestLockGuard {
370    #[doc(hidden)]
371    pub fn disable_host_managed_plot_env(&self) -> HostManagedPlotEnvGuard<'_> {
372        // The returned guard borrows `self`, so TEST_PLOT_REGISTRY_LOCK stays held
373        // for the full duration of this process-env override.
374        let previous = std::env::var_os("RUNMAT_HOST_MANAGED_PLOTS");
375        unsafe {
376            std::env::remove_var("RUNMAT_HOST_MANAGED_PLOTS");
377        }
378        HostManagedPlotEnvGuard {
379            _plot_guard: self,
380            previous,
381        }
382    }
383}
384
385#[doc(hidden)]
386pub fn lock_plot_test_registry() -> PlotTestLockGuard {
387    let guard = TEST_PLOT_REGISTRY_LOCK
388        .lock()
389        .unwrap_or_else(|e| e.into_inner());
390    TEST_PLOT_OUTER_LOCK_HELD.with(|flag| flag.set(true));
391    let disable_previous = std::env::var_os("RUNMAT_DISABLE_INTERACTIVE_PLOTS");
392    let host_previous = std::env::var_os("RUNMAT_HOST_MANAGED_PLOTS");
393    set_plot_test_env_vars();
394    PlotTestLockGuard {
395        _guard: guard,
396        disable_previous,
397        host_previous,
398    }
399}
400
401#[doc(hidden)]
402pub struct HostManagedPlotEnvGuard<'a> {
403    _plot_guard: &'a PlotTestLockGuard,
404    previous: Option<std::ffi::OsString>,
405}
406
407impl Drop for HostManagedPlotEnvGuard<'_> {
408    fn drop(&mut self) {
409        restore_env_var("RUNMAT_HOST_MANAGED_PLOTS", self.previous.take());
410    }
411}
412
413fn set_plot_test_env_vars() {
414    unsafe {
415        std::env::set_var("RUNMAT_DISABLE_INTERACTIVE_PLOTS", "1");
416        std::env::set_var("RUNMAT_HOST_MANAGED_PLOTS", "1");
417    }
418}
419
420fn restore_env_var(key: &'static str, previous: Option<std::ffi::OsString>) {
421    unsafe {
422        if let Some(previous) = previous {
423            std::env::set_var(key, previous);
424        } else {
425            std::env::remove_var(key);
426        }
427    }
428}
429
430#[cfg(target_arch = "wasm32")]
431runmat_thread_local! {
432    static REGISTRY: RefCell<PlotRegistry> = RefCell::new(PlotRegistry::default());
433}
434
435#[cfg(not(target_arch = "wasm32"))]
436type RegistryBackendGuard<'a> = MutexGuard<'a, PlotRegistry>;
437#[cfg(target_arch = "wasm32")]
438type RegistryBackendGuard<'a> = std::cell::RefMut<'a, PlotRegistry>;
439
440struct PlotRegistryGuard<'a> {
441    inner: RegistryBackendGuard<'a>,
442    #[cfg(test)]
443    _test_lock: Option<std::sync::MutexGuard<'static, ()>>,
444}
445
446impl<'a> PlotRegistryGuard<'a> {
447    #[cfg(test)]
448    fn new(
449        inner: RegistryBackendGuard<'a>,
450        _test_lock: Option<std::sync::MutexGuard<'static, ()>>,
451    ) -> Self {
452        Self { inner, _test_lock }
453    }
454
455    #[cfg(not(test))]
456    fn new(inner: RegistryBackendGuard<'a>) -> Self {
457        Self { inner }
458    }
459}
460
461impl<'a> Deref for PlotRegistryGuard<'a> {
462    type Target = PlotRegistry;
463
464    fn deref(&self) -> &Self::Target {
465        &self.inner
466    }
467}
468
469impl<'a> DerefMut for PlotRegistryGuard<'a> {
470    fn deref_mut(&mut self) -> &mut Self::Target {
471        &mut self.inner
472    }
473}
474
475const AXES_INDEX_BITS: u32 = 20;
476const AXES_INDEX_MASK: u64 = (1 << AXES_INDEX_BITS) - 1;
477const OBJECT_KIND_BITS: u32 = 4;
478const OBJECT_KIND_MASK: u64 = (1 << OBJECT_KIND_BITS) - 1;
479
480#[derive(Clone, Copy, Debug, PartialEq, Eq)]
481pub enum PlotObjectKind {
482    Title = 1,
483    XLabel = 2,
484    YLabel = 3,
485    ZLabel = 4,
486    Legend = 5,
487    SuperTitle = 6,
488}
489
490impl PlotObjectKind {
491    fn from_u64(value: u64) -> Option<Self> {
492        match value {
493            1 => Some(Self::Title),
494            2 => Some(Self::XLabel),
495            3 => Some(Self::YLabel),
496            4 => Some(Self::ZLabel),
497            5 => Some(Self::Legend),
498            6 => Some(Self::SuperTitle),
499            _ => None,
500        }
501    }
502}
503
504#[derive(Debug, Error)]
505pub enum FigureError {
506    #[error("figure handle {0} does not exist")]
507    InvalidHandle(u32),
508    #[error("subplot grid dimensions must be positive (rows={rows}, cols={cols})")]
509    InvalidSubplotGrid { rows: usize, cols: usize },
510    #[error("subplot index {index} is out of range for a {rows}x{cols} grid")]
511    InvalidSubplotIndex {
512        rows: usize,
513        cols: usize,
514        index: usize,
515    },
516    #[error("invalid axes handle")]
517    InvalidAxesHandle,
518    #[error("invalid plot object handle")]
519    InvalidPlotObjectHandle,
520    #[error("failed to render figure snapshot: {source}")]
521    RenderFailure {
522        #[source]
523        source: Box<dyn std::error::Error + Send + Sync>,
524    },
525}
526
527fn map_figure_error(builtin: &'static str, err: FigureError) -> RuntimeError {
528    let message = format!("{builtin}: {err}");
529    plotting_error_with_source(builtin, message, err)
530}
531
532pub(crate) fn clear_figure_with_builtin(
533    builtin: &'static str,
534    target: Option<FigureHandle>,
535) -> BuiltinResult<FigureHandle> {
536    clear_figure(target).map_err(|err| map_figure_error(builtin, err))
537}
538
539pub(crate) fn close_figure_with_builtin(
540    builtin: &'static str,
541    target: Option<FigureHandle>,
542) -> BuiltinResult<FigureHandle> {
543    close_figure(target).map_err(|err| map_figure_error(builtin, err))
544}
545
546pub fn set_grid_enabled(enabled: bool) {
547    let (handle, figure_clone) = {
548        let mut reg = registry();
549        let handle = reg.current;
550        let state = get_state_mut(&mut reg, handle);
551        let axes = state.active_axes;
552        state.figure.set_axes_grid_enabled(axes, enabled);
553        state.revision = state.revision.wrapping_add(1);
554        (handle, state.figure.clone())
555    };
556    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
557}
558
559pub fn set_grid_and_minor_grid_enabled(grid_enabled: bool, minor_grid_enabled: Option<bool>) {
560    let (handle, figure_clone) = {
561        let mut reg = registry();
562        let handle = reg.current;
563        let state = get_state_mut(&mut reg, handle);
564        let axes = state.active_axes;
565        state.figure.set_axes_grid_enabled(axes, grid_enabled);
566        if let Some(minor_enabled) = minor_grid_enabled {
567            state
568                .figure
569                .set_axes_minor_grid_enabled(axes, minor_enabled);
570        }
571        state.revision = state.revision.wrapping_add(1);
572        (handle, state.figure.clone())
573    };
574    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
575}
576
577pub fn set_grid_enabled_for_axes(
578    handle: FigureHandle,
579    axes_index: usize,
580    enabled: bool,
581) -> Result<(), FigureError> {
582    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
583        state.figure.set_axes_grid_enabled(axes_index, enabled);
584    })?;
585    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
586    Ok(())
587}
588
589pub fn set_minor_grid_enabled_for_axes(
590    handle: FigureHandle,
591    axes_index: usize,
592    enabled: bool,
593) -> Result<(), FigureError> {
594    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
595        state
596            .figure
597            .set_axes_minor_grid_enabled(axes_index, enabled);
598    })?;
599    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
600    Ok(())
601}
602
603pub fn toggle_grid() -> bool {
604    let (handle, figure_clone, enabled) = {
605        let mut reg = registry();
606        let handle = reg.current;
607        let state = get_state_mut(&mut reg, handle);
608        let axes = state.active_axes;
609        let next = !state
610            .figure
611            .axes_metadata(axes)
612            .map(|m| m.grid_enabled)
613            .unwrap_or(true);
614        state.figure.set_axes_grid_enabled(axes, next);
615        state.revision = state.revision.wrapping_add(1);
616        (handle, state.figure.clone(), next)
617    };
618    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
619    enabled
620}
621
622pub fn toggle_minor_grid() -> bool {
623    let (handle, figure_clone, enabled) = {
624        let mut reg = registry();
625        let handle = reg.current;
626        let state = get_state_mut(&mut reg, handle);
627        let axes = state.active_axes;
628        let next = !state.figure.minor_grid_enabled_for_axes(axes);
629        state.figure.set_axes_minor_grid_enabled(axes, next);
630        state.revision = state.revision.wrapping_add(1);
631        (handle, state.figure.clone(), next)
632    };
633    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
634    enabled
635}
636
637pub fn set_box_enabled(enabled: bool) {
638    let (handle, figure_clone) = {
639        let mut reg = registry();
640        let handle = reg.current;
641        let state = get_state_mut(&mut reg, handle);
642        let axes = state.active_axes;
643        state.figure.set_axes_box_enabled(axes, enabled);
644        state.revision = state.revision.wrapping_add(1);
645        (handle, state.figure.clone())
646    };
647    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
648}
649
650pub fn set_box_enabled_for_axes(
651    handle: FigureHandle,
652    axes_index: usize,
653    enabled: bool,
654) -> Result<(), FigureError> {
655    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
656        state.figure.set_axes_box_enabled(axes_index, enabled);
657    })?;
658    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
659    Ok(())
660}
661
662pub fn set_axes_style_for_axes(
663    handle: FigureHandle,
664    axes_index: usize,
665    style: TextStyle,
666) -> Result<(), FigureError> {
667    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
668        state.figure.set_axes_style(axes_index, style);
669    })?;
670    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
671    Ok(())
672}
673
674pub fn set_figure_title_for_axes(
675    handle: FigureHandle,
676    axes_index: usize,
677    title: &str,
678    style: TextStyle,
679) -> Result<f64, FigureError> {
680    let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
681        state.figure.set_axes_title(axes_index, title.to_string());
682        state.figure.set_axes_title_style(axes_index, style);
683        encode_plot_object_handle(handle, axes_index, PlotObjectKind::Title)
684    })?;
685    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
686    Ok(object_handle)
687}
688
689pub fn set_sg_title_for_figure(
690    handle: FigureHandle,
691    title: &str,
692    style: TextStyle,
693) -> Result<f64, FigureError> {
694    let (object_handle, figure_clone) = with_figure_mut(handle, |state| {
695        state.figure.set_sg_title(title.to_string());
696        state.figure.set_sg_title_style(style);
697        encode_plot_object_handle(handle, 0, PlotObjectKind::SuperTitle)
698    })?;
699    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
700    Ok(object_handle)
701}
702
703pub fn set_sg_title_properties_for_figure(
704    handle: FigureHandle,
705    text: Option<String>,
706    style: Option<TextStyle>,
707) -> Result<f64, FigureError> {
708    let (object_handle, figure_clone) = with_figure_mut(handle, |state| {
709        if let Some(text) = text {
710            state.figure.set_sg_title(text);
711        }
712        if let Some(style) = style {
713            state.figure.set_sg_title_style(style);
714        }
715        encode_plot_object_handle(handle, 0, PlotObjectKind::SuperTitle)
716    })?;
717    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
718    Ok(object_handle)
719}
720
721pub fn set_figure_name(handle: FigureHandle, name: String) -> Result<(), FigureError> {
722    let ((), figure_clone) = with_figure_mut(handle, |state| {
723        state.figure.set_name(name);
724    })?;
725    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
726    Ok(())
727}
728
729pub fn set_figure_number_title(handle: FigureHandle, enabled: bool) -> Result<(), FigureError> {
730    let ((), figure_clone) = with_figure_mut(handle, |state| {
731        state.figure.set_number_title(enabled);
732    })?;
733    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
734    Ok(())
735}
736
737pub fn set_figure_visible(
738    handle: FigureHandle,
739    visible: bool,
740) -> Result<(bool, Figure), FigureError> {
741    let ((was_visible, now_visible), figure_clone) = with_figure_mut(handle, |state| {
742        let was_visible = state.figure.visible;
743        state.figure.set_visible(visible);
744        (was_visible, state.figure.visible)
745    })?;
746    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
747    Ok((!was_visible && now_visible, figure_clone))
748}
749
750pub fn set_figure_background_color(handle: FigureHandle, color: Vec4) -> Result<(), FigureError> {
751    let ((), figure_clone) = with_figure_mut(handle, |state| {
752        state.figure.set_background_color(color);
753    })?;
754    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
755    Ok(())
756}
757
758pub fn set_text_properties_for_axes(
759    handle: FigureHandle,
760    axes_index: usize,
761    kind: PlotObjectKind,
762    text: Option<String>,
763    style: Option<TextStyle>,
764) -> Result<f64, FigureError> {
765    let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
766        if let Some(text) = text {
767            match kind {
768                PlotObjectKind::Title => state.figure.set_axes_title(axes_index, text),
769                PlotObjectKind::XLabel => state.figure.set_axes_xlabel(axes_index, text),
770                PlotObjectKind::YLabel => state.figure.set_axes_ylabel(axes_index, text),
771                PlotObjectKind::ZLabel => state.figure.set_axes_zlabel(axes_index, text),
772                PlotObjectKind::Legend => {}
773                PlotObjectKind::SuperTitle => state.figure.set_sg_title(text),
774            }
775        }
776        if let Some(style) = style {
777            match kind {
778                PlotObjectKind::Title => state.figure.set_axes_title_style(axes_index, style),
779                PlotObjectKind::XLabel => state.figure.set_axes_xlabel_style(axes_index, style),
780                PlotObjectKind::YLabel => state.figure.set_axes_ylabel_style(axes_index, style),
781                PlotObjectKind::ZLabel => state.figure.set_axes_zlabel_style(axes_index, style),
782                PlotObjectKind::Legend => {}
783                PlotObjectKind::SuperTitle => state.figure.set_sg_title_style(style),
784            }
785        }
786        encode_plot_object_handle(handle, axes_index, kind)
787    })?;
788    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
789    Ok(object_handle)
790}
791
792pub fn set_xlabel_for_axes(
793    handle: FigureHandle,
794    axes_index: usize,
795    label: &str,
796    style: TextStyle,
797) -> Result<f64, FigureError> {
798    let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
799        state.figure.set_axes_xlabel(axes_index, label.to_string());
800        state.figure.set_axes_xlabel_style(axes_index, style);
801        encode_plot_object_handle(handle, axes_index, PlotObjectKind::XLabel)
802    })?;
803    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
804    Ok(object_handle)
805}
806
807pub fn set_ylabel_for_axes(
808    handle: FigureHandle,
809    axes_index: usize,
810    label: &str,
811    style: TextStyle,
812) -> Result<f64, FigureError> {
813    let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
814        state.figure.set_axes_ylabel(axes_index, label.to_string());
815        state.figure.set_axes_ylabel_style(axes_index, style);
816        encode_plot_object_handle(handle, axes_index, PlotObjectKind::YLabel)
817    })?;
818    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
819    Ok(object_handle)
820}
821
822pub fn set_zlabel_for_axes(
823    handle: FigureHandle,
824    axes_index: usize,
825    label: &str,
826    style: TextStyle,
827) -> Result<f64, FigureError> {
828    let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
829        state.figure.set_axes_zlabel(axes_index, label.to_string());
830        state.figure.set_axes_zlabel_style(axes_index, style);
831        encode_plot_object_handle(handle, axes_index, PlotObjectKind::ZLabel)
832    })?;
833    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
834    Ok(object_handle)
835}
836
837pub fn add_text_annotation_for_axes(
838    handle: FigureHandle,
839    axes_index: usize,
840    position: glam::Vec3,
841    text: &str,
842    style: TextStyle,
843) -> Result<f64, FigureError> {
844    let (annotation_index, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
845        state
846            .figure
847            .add_axes_text_annotation(axes_index, position, text.to_string(), style)
848    })?;
849    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
850    Ok(register_text_annotation_handle(
851        handle,
852        axes_index,
853        annotation_index,
854    ))
855}
856
857pub fn set_text_annotation_properties_for_axes(
858    handle: FigureHandle,
859    axes_index: usize,
860    annotation_index: usize,
861    text: Option<String>,
862    position: Option<glam::Vec3>,
863    style: Option<TextStyle>,
864) -> Result<f64, FigureError> {
865    let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
866        if let Some(text) = text {
867            state
868                .figure
869                .set_axes_text_annotation_text(axes_index, annotation_index, text);
870        }
871        if let Some(position) = position {
872            state
873                .figure
874                .set_axes_text_annotation_position(axes_index, annotation_index, position);
875        }
876        if let Some(style) = style {
877            state
878                .figure
879                .set_axes_text_annotation_style(axes_index, annotation_index, style);
880        }
881        register_text_annotation_handle(handle, axes_index, annotation_index)
882    })?;
883    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
884    Ok(object_handle)
885}
886
887pub fn toggle_box() -> bool {
888    let (handle, figure_clone, enabled) = {
889        let mut reg = registry();
890        let handle = reg.current;
891        let state = get_state_mut(&mut reg, handle);
892        let axes = state.active_axes;
893        let next = !state
894            .figure
895            .axes_metadata(axes)
896            .map(|m| m.box_enabled)
897            .unwrap_or(true);
898        state.figure.set_axes_box_enabled(axes, next);
899        state.revision = state.revision.wrapping_add(1);
900        (handle, state.figure.clone(), next)
901    };
902    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
903    enabled
904}
905
906pub fn set_axis_equal(enabled: bool) {
907    let (handle, figure_clone) = {
908        let mut reg = registry();
909        let handle = reg.current;
910        let state = get_state_mut(&mut reg, handle);
911        let axes = state.active_axes;
912        state.figure.set_axes_axis_equal(axes, enabled);
913        state.revision = state.revision.wrapping_add(1);
914        (handle, state.figure.clone())
915    };
916    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
917}
918
919pub fn set_axis_equal_and_limits(enabled: bool, x: Option<(f64, f64)>, y: Option<(f64, f64)>) {
920    let (handle, figure_clone) = {
921        let mut reg = registry();
922        let handle = reg.current;
923        let state = get_state_mut(&mut reg, handle);
924        let axes = state.active_axes;
925        state.figure.set_axes_axis_equal(axes, enabled);
926        state.figure.set_axes_limits(axes, x, y);
927        state.revision = state.revision.wrapping_add(1);
928        (handle, state.figure.clone())
929    };
930    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
931}
932
933pub fn set_axis_equal_for_axes(
934    handle: FigureHandle,
935    axes_index: usize,
936    enabled: bool,
937) -> Result<(), FigureError> {
938    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
939        state.figure.set_axes_axis_equal(axes_index, enabled);
940    })?;
941    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
942    Ok(())
943}
944
945pub fn set_axis_limits(x: Option<(f64, f64)>, y: Option<(f64, f64)>) {
946    let (handle, figure_clone) = {
947        let mut reg = registry();
948        let handle = reg.current;
949        let state = get_state_mut(&mut reg, handle);
950        let axes = state.active_axes;
951        state.figure.set_axes_limits(axes, x, y);
952        state.revision = state.revision.wrapping_add(1);
953        (handle, state.figure.clone())
954    };
955    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
956}
957
958pub fn set_axis_limits_for_axes(
959    handle: FigureHandle,
960    axes_index: usize,
961    x: Option<(f64, f64)>,
962    y: Option<(f64, f64)>,
963) -> Result<(), FigureError> {
964    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
965        state.figure.set_axes_limits(axes_index, x, y);
966    })?;
967    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
968    Ok(())
969}
970
971pub fn axis_limits_snapshot() -> AxisLimitSnapshot {
972    let mut reg = registry();
973    let handle = reg.current;
974    let state = get_state_mut(&mut reg, handle);
975    let axes = state.active_axes;
976    let meta = state
977        .figure
978        .axes_metadata(axes)
979        .cloned()
980        .unwrap_or_default();
981    (meta.x_limits, meta.y_limits)
982}
983
984pub fn z_limits_snapshot() -> Option<(f64, f64)> {
985    let mut reg = registry();
986    let handle = reg.current;
987    let state = get_state_mut(&mut reg, handle);
988    let axes = state.active_axes;
989    state.figure.axes_metadata(axes).and_then(|m| m.z_limits)
990}
991
992pub fn color_limits_snapshot() -> Option<(f64, f64)> {
993    let mut reg = registry();
994    let handle = reg.current;
995    let state = get_state_mut(&mut reg, handle);
996    let axes = state.active_axes;
997    state
998        .figure
999        .axes_metadata(axes)
1000        .and_then(|m| m.color_limits)
1001}
1002
1003pub fn set_z_limits(limits: Option<(f64, f64)>) {
1004    let (handle, figure_clone) = {
1005        let mut reg = registry();
1006        let handle = reg.current;
1007        let state = get_state_mut(&mut reg, handle);
1008        let axes = state.active_axes;
1009        state.figure.set_axes_z_limits(axes, limits);
1010        state.revision = state.revision.wrapping_add(1);
1011        (handle, state.figure.clone())
1012    };
1013    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1014}
1015
1016pub fn set_z_limits_for_axes(
1017    handle: FigureHandle,
1018    axes_index: usize,
1019    limits: Option<(f64, f64)>,
1020) -> Result<(), FigureError> {
1021    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
1022        state.figure.set_axes_z_limits(axes_index, limits);
1023    })?;
1024    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1025    Ok(())
1026}
1027
1028pub fn set_color_limits_runtime(limits: Option<(f64, f64)>) {
1029    let (handle, figure_clone) = {
1030        let mut reg = registry();
1031        let handle = reg.current;
1032        let state = get_state_mut(&mut reg, handle);
1033        let axes = state.active_axes;
1034        state.figure.set_axes_color_limits(axes, limits);
1035        state.revision = state.revision.wrapping_add(1);
1036        (handle, state.figure.clone())
1037    };
1038    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1039}
1040
1041pub fn set_color_limits_for_axes(
1042    handle: FigureHandle,
1043    axes_index: usize,
1044    limits: Option<(f64, f64)>,
1045) -> Result<(), FigureError> {
1046    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
1047        state.figure.set_axes_color_limits(axes_index, limits);
1048    })?;
1049    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1050    Ok(())
1051}
1052
1053pub fn clear_current_axes() {
1054    let (handle, figure_clone) = {
1055        let mut reg = registry();
1056        let handle = reg.current;
1057        let axes_index = {
1058            let state = get_state_mut(&mut reg, handle);
1059            let axes_index = state.active_axes;
1060            state.figure.clear_axes(axes_index);
1061            state.reset_cycle(axes_index);
1062            state.revision = state.revision.wrapping_add(1);
1063            axes_index
1064        };
1065        purge_plot_children_for_axes(&mut reg, handle, axes_index);
1066        let figure_clone = reg
1067            .figures
1068            .get(&handle)
1069            .expect("figure exists")
1070            .figure
1071            .clone();
1072        (handle, figure_clone)
1073    };
1074    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1075}
1076
1077pub fn set_colorbar_enabled(enabled: bool) {
1078    let (handle, figure_clone) = {
1079        let mut reg = registry();
1080        let handle = reg.current;
1081        let state = get_state_mut(&mut reg, handle);
1082        let axes = state.active_axes;
1083        state.figure.set_axes_colorbar_enabled(axes, enabled);
1084        state.revision = state.revision.wrapping_add(1);
1085        (handle, state.figure.clone())
1086    };
1087    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1088}
1089
1090pub fn set_colorbar_enabled_for_axes(
1091    handle: FigureHandle,
1092    axes_index: usize,
1093    enabled: bool,
1094) -> Result<(), FigureError> {
1095    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
1096        state.figure.set_axes_colorbar_enabled(axes_index, enabled);
1097    })?;
1098    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1099    Ok(())
1100}
1101
1102pub fn set_legend_for_axes(
1103    handle: FigureHandle,
1104    axes_index: usize,
1105    enabled: bool,
1106    labels: Option<&[String]>,
1107    style: Option<LegendStyle>,
1108) -> Result<f64, FigureError> {
1109    let (object_handle, figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
1110        state.figure.set_axes_legend_enabled(axes_index, enabled);
1111        if let Some(labels) = labels {
1112            state.figure.set_labels_for_axes(axes_index, labels);
1113        }
1114        if let Some(style) = style {
1115            state.figure.set_axes_legend_style(axes_index, style);
1116        }
1117        encode_plot_object_handle(handle, axes_index, PlotObjectKind::Legend)
1118    })?;
1119    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1120    Ok(object_handle)
1121}
1122
1123pub fn set_log_modes_for_axes(
1124    handle: FigureHandle,
1125    axes_index: usize,
1126    x_log: bool,
1127    y_log: bool,
1128) -> Result<(), FigureError> {
1129    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
1130        state.figure.set_axes_log_modes(axes_index, x_log, y_log);
1131    })?;
1132    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1133    Ok(())
1134}
1135
1136pub fn set_view_for_axes(
1137    handle: FigureHandle,
1138    axes_index: usize,
1139    azimuth_deg: f32,
1140    elevation_deg: f32,
1141) -> Result<(), FigureError> {
1142    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
1143        state
1144            .figure
1145            .set_axes_view(axes_index, azimuth_deg, elevation_deg);
1146    })?;
1147    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1148    Ok(())
1149}
1150
1151pub fn legend_entries_snapshot(
1152    handle: FigureHandle,
1153    axes_index: usize,
1154) -> Result<Vec<runmat_plot::plots::LegendEntry>, FigureError> {
1155    let mut reg = registry();
1156    let state = get_state_mut(&mut reg, handle);
1157    let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
1158    if axes_index >= total_axes {
1159        return Err(FigureError::InvalidSubplotIndex {
1160            rows: state.figure.axes_rows.max(1),
1161            cols: state.figure.axes_cols.max(1),
1162            index: axes_index,
1163        });
1164    }
1165    Ok(state.figure.legend_entries_for_axes(axes_index))
1166}
1167
1168pub fn toggle_colorbar() -> bool {
1169    let (handle, figure_clone, enabled) = {
1170        let mut reg = registry();
1171        let handle = reg.current;
1172        let state = get_state_mut(&mut reg, handle);
1173        let axes = state.active_axes;
1174        let next = !state
1175            .figure
1176            .axes_metadata(axes)
1177            .map(|m| m.colorbar_enabled)
1178            .unwrap_or(false);
1179        state.figure.set_axes_colorbar_enabled(axes, next);
1180        state.revision = state.revision.wrapping_add(1);
1181        (handle, state.figure.clone(), next)
1182    };
1183    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1184    enabled
1185}
1186
1187pub fn set_colormap(colormap: ColorMap) {
1188    let (handle, figure_clone) = {
1189        let mut reg = registry();
1190        let handle = reg.current;
1191        let state = get_state_mut(&mut reg, handle);
1192        let axes = state.active_axes;
1193        state.figure.set_axes_colormap(axes, colormap);
1194        state.revision = state.revision.wrapping_add(1);
1195        (handle, state.figure.clone())
1196    };
1197    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1198}
1199
1200pub fn set_colormap_for_axes(
1201    handle: FigureHandle,
1202    axes_index: usize,
1203    colormap: ColorMap,
1204) -> Result<(), FigureError> {
1205    let ((), figure_clone) = with_axes_target_mut(handle, axes_index, |state| {
1206        state.figure.set_axes_colormap(axes_index, colormap);
1207    })?;
1208    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1209    Ok(())
1210}
1211
1212pub fn set_surface_shading(mode: ShadingMode) {
1213    let (handle, figure_clone) = {
1214        let mut reg = registry();
1215        let handle = reg.current;
1216        let state = get_state_mut(&mut reg, handle);
1217        let plot_count = state.figure.len();
1218        for idx in 0..plot_count {
1219            if let Some(PlotElement::Surface(surface)) = state.figure.get_plot_mut(idx) {
1220                *surface = surface.clone().with_shading(mode);
1221            }
1222        }
1223        state.revision = state.revision.wrapping_add(1);
1224        (handle, state.figure.clone())
1225    };
1226    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
1227}
1228
1229#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1230pub enum FigureEventKind {
1231    Created,
1232    Updated,
1233    Cleared,
1234    Closed,
1235}
1236
1237#[derive(Clone, Copy)]
1238pub struct FigureEventView<'a> {
1239    pub handle: FigureHandle,
1240    pub kind: FigureEventKind,
1241    pub revision: Option<u64>,
1242    pub figure: Option<&'a Figure>,
1243}
1244
1245type FigureObserver = dyn for<'a> Fn(FigureEventView<'a>) + Send + Sync + 'static;
1246
1247struct FigureObserverRegistry {
1248    observers: Mutex<Vec<Arc<FigureObserver>>>,
1249}
1250
1251impl FigureObserverRegistry {
1252    fn new() -> Self {
1253        Self {
1254            observers: Mutex::new(Vec::new()),
1255        }
1256    }
1257
1258    fn install(&self, observer: Arc<FigureObserver>) {
1259        let mut guard = self.observers.lock().expect("figure observers poisoned");
1260        guard.push(observer);
1261    }
1262
1263    fn notify(&self, view: FigureEventView<'_>) {
1264        let snapshot = {
1265            let guard = self.observers.lock().expect("figure observers poisoned");
1266            guard.clone()
1267        };
1268        for observer in snapshot {
1269            observer(view);
1270        }
1271    }
1272
1273    fn is_empty(&self) -> bool {
1274        self.observers
1275            .lock()
1276            .map(|guard| guard.is_empty())
1277            .unwrap_or(true)
1278    }
1279}
1280
1281static FIGURE_OBSERVERS: OnceCell<FigureObserverRegistry> = OnceCell::new();
1282
1283runmat_thread_local! {
1284    static RECENT_FIGURES: RefCell<HashSet<FigureHandle>> = RefCell::new(HashSet::new());
1285    static ACTIVE_AXES_CONTEXT: RefCell<Option<ActiveAxesContext>> = const { RefCell::new(None) };
1286}
1287
1288#[derive(Clone, Copy, Debug)]
1289pub struct FigureAxesState {
1290    pub handle: FigureHandle,
1291    pub rows: usize,
1292    pub cols: usize,
1293    pub active_index: usize,
1294}
1295
1296pub fn encode_axes_handle(handle: FigureHandle, axes_index: usize) -> f64 {
1297    let encoded =
1298        ((handle.as_u32() as u64) << AXES_INDEX_BITS) | ((axes_index as u64) & AXES_INDEX_MASK);
1299    encoded as f64
1300}
1301
1302pub fn encode_plot_object_handle(
1303    handle: FigureHandle,
1304    axes_index: usize,
1305    kind: PlotObjectKind,
1306) -> f64 {
1307    let encoded = (((handle.as_u32() as u64) << AXES_INDEX_BITS)
1308        | ((axes_index as u64) & AXES_INDEX_MASK))
1309        << OBJECT_KIND_BITS
1310        | ((kind as u64) & OBJECT_KIND_MASK);
1311    encoded as f64
1312}
1313
1314pub fn decode_plot_object_handle(
1315    value: f64,
1316) -> Result<(FigureHandle, usize, PlotObjectKind), FigureError> {
1317    if !value.is_finite() || value <= 0.0 {
1318        return Err(FigureError::InvalidPlotObjectHandle);
1319    }
1320    let encoded = value.round() as u64;
1321    let kind = PlotObjectKind::from_u64(encoded & OBJECT_KIND_MASK)
1322        .ok_or(FigureError::InvalidPlotObjectHandle)?;
1323    let base = encoded >> OBJECT_KIND_BITS;
1324    let figure_id = base >> AXES_INDEX_BITS;
1325    if figure_id == 0 {
1326        return Err(FigureError::InvalidPlotObjectHandle);
1327    }
1328    let axes_index = (base & AXES_INDEX_MASK) as usize;
1329    Ok((FigureHandle::from(figure_id as u32), axes_index, kind))
1330}
1331
1332pub fn register_histogram_handle(
1333    figure: FigureHandle,
1334    axes_index: usize,
1335    plot_index: usize,
1336    bin_edges: Vec<f64>,
1337    raw_counts: Vec<f64>,
1338    normalization: String,
1339) -> f64 {
1340    let mut reg = registry();
1341    let id = reg.next_plot_child_handle;
1342    reg.next_plot_child_handle += 1;
1343    reg.plot_children.insert(
1344        id,
1345        PlotChildHandleState::Histogram(HistogramHandleState {
1346            figure,
1347            axes_index,
1348            plot_index,
1349            bin_edges,
1350            raw_counts,
1351            normalization,
1352            display_name: None,
1353        }),
1354    );
1355    id as f64
1356}
1357
1358fn register_simple_plot_handle(
1359    figure: FigureHandle,
1360    axes_index: usize,
1361    plot_index: usize,
1362    constructor: fn(SimplePlotHandleState) -> PlotChildHandleState,
1363) -> f64 {
1364    let mut reg = registry();
1365    let id = reg.next_plot_child_handle;
1366    reg.next_plot_child_handle += 1;
1367    reg.plot_children.insert(
1368        id,
1369        constructor(SimplePlotHandleState {
1370            figure,
1371            axes_index,
1372            plot_index,
1373        }),
1374    );
1375    id as f64
1376}
1377
1378pub fn register_line_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1379    register_simple_plot_handle(figure, axes_index, plot_index, PlotChildHandleState::Line)
1380}
1381
1382pub fn register_reference_line_handle(
1383    figure: FigureHandle,
1384    axes_index: usize,
1385    plot_index: usize,
1386) -> f64 {
1387    register_simple_plot_handle(
1388        figure,
1389        axes_index,
1390        plot_index,
1391        PlotChildHandleState::ReferenceLine,
1392    )
1393}
1394
1395pub fn register_scatter_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1396    register_simple_plot_handle(
1397        figure,
1398        axes_index,
1399        plot_index,
1400        PlotChildHandleState::Scatter,
1401    )
1402}
1403
1404pub fn register_bar_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1405    register_simple_plot_handle(figure, axes_index, plot_index, PlotChildHandleState::Bar)
1406}
1407
1408pub fn register_stem_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1409    register_simple_plot_handle(figure, axes_index, plot_index, |state| {
1410        PlotChildHandleState::Stem(StemHandleState {
1411            figure: state.figure,
1412            axes_index: state.axes_index,
1413            plot_index: state.plot_index,
1414        })
1415    })
1416}
1417
1418pub fn register_errorbar_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1419    register_simple_plot_handle(figure, axes_index, plot_index, |state| {
1420        PlotChildHandleState::ErrorBar(ErrorBarHandleState {
1421            figure: state.figure,
1422            axes_index: state.axes_index,
1423            plot_index: state.plot_index,
1424        })
1425    })
1426}
1427
1428pub fn register_stairs_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1429    register_simple_plot_handle(figure, axes_index, plot_index, PlotChildHandleState::Stairs)
1430}
1431
1432pub fn register_quiver_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1433    register_simple_plot_handle(figure, axes_index, plot_index, |state| {
1434        PlotChildHandleState::Quiver(QuiverHandleState {
1435            figure: state.figure,
1436            axes_index: state.axes_index,
1437            plot_index: state.plot_index,
1438        })
1439    })
1440}
1441
1442pub fn register_image_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1443    register_simple_plot_handle(figure, axes_index, plot_index, |state| {
1444        PlotChildHandleState::Image(ImageHandleState {
1445            figure: state.figure,
1446            axes_index: state.axes_index,
1447            plot_index: state.plot_index,
1448        })
1449    })
1450}
1451
1452pub fn register_heatmap_handle(
1453    figure: FigureHandle,
1454    axes_index: usize,
1455    plot_index: usize,
1456    x_labels: Vec<String>,
1457    y_labels: Vec<String>,
1458    color_data: Tensor,
1459) -> f64 {
1460    let mut reg = registry();
1461    let id = reg.next_plot_child_handle;
1462    reg.next_plot_child_handle += 1;
1463    reg.plot_children.insert(
1464        id,
1465        PlotChildHandleState::Heatmap(HeatmapHandleState {
1466            figure,
1467            axes_index,
1468            plot_index,
1469            x_labels,
1470            y_labels,
1471            color_data,
1472        }),
1473    );
1474    id as f64
1475}
1476
1477pub fn register_area_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1478    register_simple_plot_handle(figure, axes_index, plot_index, |state| {
1479        PlotChildHandleState::Area(AreaHandleState {
1480            figure: state.figure,
1481            axes_index: state.axes_index,
1482            plot_index: state.plot_index,
1483        })
1484    })
1485}
1486
1487pub fn register_surface_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1488    register_simple_plot_handle(
1489        figure,
1490        axes_index,
1491        plot_index,
1492        PlotChildHandleState::Surface,
1493    )
1494}
1495
1496pub fn register_patch_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1497    register_simple_plot_handle(figure, axes_index, plot_index, PlotChildHandleState::Patch)
1498}
1499
1500pub fn register_line3_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1501    register_simple_plot_handle(figure, axes_index, plot_index, PlotChildHandleState::Line3)
1502}
1503
1504pub fn register_scatter3_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1505    register_simple_plot_handle(
1506        figure,
1507        axes_index,
1508        plot_index,
1509        PlotChildHandleState::Scatter3,
1510    )
1511}
1512
1513pub fn register_contour_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1514    register_simple_plot_handle(
1515        figure,
1516        axes_index,
1517        plot_index,
1518        PlotChildHandleState::Contour,
1519    )
1520}
1521
1522pub fn register_contour_fill_handle(
1523    figure: FigureHandle,
1524    axes_index: usize,
1525    plot_index: usize,
1526) -> f64 {
1527    register_simple_plot_handle(
1528        figure,
1529        axes_index,
1530        plot_index,
1531        PlotChildHandleState::ContourFill,
1532    )
1533}
1534
1535pub fn register_pie_handle(figure: FigureHandle, axes_index: usize, plot_index: usize) -> f64 {
1536    register_simple_plot_handle(figure, axes_index, plot_index, PlotChildHandleState::Pie)
1537}
1538
1539pub fn register_text_annotation_handle(
1540    figure: FigureHandle,
1541    axes_index: usize,
1542    annotation_index: usize,
1543) -> f64 {
1544    let mut reg = registry();
1545    let id = reg.next_plot_child_handle;
1546    reg.next_plot_child_handle += 1;
1547    reg.plot_children.insert(
1548        id,
1549        PlotChildHandleState::Text(TextAnnotationHandleState {
1550            figure,
1551            axes_index,
1552            annotation_index,
1553        }),
1554    );
1555    id as f64
1556}
1557
1558pub fn plot_child_handle_snapshot(handle: f64) -> Result<PlotChildHandleState, FigureError> {
1559    if !handle.is_finite() || handle <= 0.0 {
1560        return Err(FigureError::InvalidPlotObjectHandle);
1561    }
1562    let reg = registry();
1563    reg.plot_children
1564        .get(&(handle.round() as u64))
1565        .cloned()
1566        .ok_or(FigureError::InvalidPlotObjectHandle)
1567}
1568
1569pub fn set_heatmap_display_labels(
1570    figure: FigureHandle,
1571    axes_index: usize,
1572    plot_index: usize,
1573    x_labels: Option<Vec<String>>,
1574    y_labels: Option<Vec<String>>,
1575) -> Result<(), FigureError> {
1576    let figure_clone = {
1577        let mut reg = registry();
1578        let (current_x_labels, current_y_labels) = {
1579            let state = reg.plot_children.values_mut().find(|state| match state {
1580                PlotChildHandleState::Heatmap(heatmap) => {
1581                    heatmap.figure == figure
1582                        && heatmap.axes_index == axes_index
1583                        && heatmap.plot_index == plot_index
1584                }
1585                _ => false,
1586            });
1587            let PlotChildHandleState::Heatmap(heatmap) =
1588                state.ok_or(FigureError::InvalidPlotObjectHandle)?
1589            else {
1590                return Err(FigureError::InvalidPlotObjectHandle);
1591            };
1592            if let Some(labels) = x_labels {
1593                heatmap.x_labels = labels;
1594            }
1595            if let Some(labels) = y_labels {
1596                heatmap.y_labels = labels;
1597            }
1598            (heatmap.x_labels.clone(), heatmap.y_labels.clone())
1599        };
1600
1601        let state = get_state_mut(&mut reg, figure);
1602        let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
1603        if axes_index >= total_axes {
1604            return Err(FigureError::InvalidSubplotIndex {
1605                rows: state.figure.axes_rows.max(1),
1606                cols: state.figure.axes_cols.max(1),
1607                index: axes_index,
1608            });
1609        }
1610        state.active_axes = axes_index;
1611        state.figure.set_active_axes_index(axes_index);
1612        state.figure.set_axes_tick_labels(
1613            axes_index,
1614            Some(current_x_labels),
1615            Some(current_y_labels),
1616        );
1617        state.revision = state.revision.wrapping_add(1);
1618        state.figure.clone()
1619    };
1620    notify_with_figure(figure, &figure_clone, FigureEventKind::Updated);
1621    Ok(())
1622}
1623
1624pub fn update_histogram_handle_for_plot(
1625    figure: FigureHandle,
1626    axes_index: usize,
1627    plot_index: usize,
1628    normalization: String,
1629    raw_counts: Vec<f64>,
1630) -> Result<(), FigureError> {
1631    let mut reg = registry();
1632    let state = reg.plot_children.values_mut().find(|state| match state {
1633        PlotChildHandleState::Histogram(hist) => {
1634            hist.figure == figure && hist.axes_index == axes_index && hist.plot_index == plot_index
1635        }
1636        _ => false,
1637    });
1638    match state.ok_or(FigureError::InvalidPlotObjectHandle)? {
1639        PlotChildHandleState::Histogram(hist) => {
1640            hist.normalization = normalization;
1641            hist.raw_counts = raw_counts;
1642            Ok(())
1643        }
1644        _ => Err(FigureError::InvalidPlotObjectHandle),
1645    }
1646}
1647
1648pub fn set_histogram_handle_display_name(
1649    figure: FigureHandle,
1650    axes_index: usize,
1651    plot_index: usize,
1652    display_name: Option<String>,
1653) -> Result<(), FigureError> {
1654    let mut reg = registry();
1655    let state = reg.plot_children.values_mut().find(|state| match state {
1656        PlotChildHandleState::Histogram(hist) => {
1657            hist.figure == figure && hist.axes_index == axes_index && hist.plot_index == plot_index
1658        }
1659        _ => false,
1660    });
1661    match state.ok_or(FigureError::InvalidPlotObjectHandle)? {
1662        PlotChildHandleState::Histogram(hist) => {
1663            hist.display_name = display_name;
1664            Ok(())
1665        }
1666        _ => Err(FigureError::InvalidPlotObjectHandle),
1667    }
1668}
1669
1670pub fn update_errorbar_plot(
1671    figure_handle: FigureHandle,
1672    plot_index: usize,
1673    updater: impl FnOnce(&mut runmat_plot::plots::ErrorBar),
1674) -> Result<(), FigureError> {
1675    let mut reg = registry();
1676    let state = get_state_mut(&mut reg, figure_handle);
1677    let plot = state
1678        .figure
1679        .get_plot_mut(plot_index)
1680        .ok_or(FigureError::InvalidPlotObjectHandle)?;
1681    match plot {
1682        runmat_plot::plots::figure::PlotElement::ErrorBar(errorbar) => {
1683            updater(errorbar);
1684            Ok(())
1685        }
1686        _ => Err(FigureError::InvalidPlotObjectHandle),
1687    }
1688}
1689
1690pub fn update_histogram_plot_data(
1691    figure_handle: FigureHandle,
1692    plot_index: usize,
1693    labels: Vec<String>,
1694    values: Vec<f64>,
1695) -> Result<(), FigureError> {
1696    let mut reg = registry();
1697    let state = get_state_mut(&mut reg, figure_handle);
1698    let plot = state
1699        .figure
1700        .get_plot_mut(plot_index)
1701        .ok_or(FigureError::InvalidPlotObjectHandle)?;
1702    match plot {
1703        runmat_plot::plots::figure::PlotElement::Bar(bar) => {
1704            bar.set_data(labels, values)
1705                .map_err(|_| FigureError::InvalidPlotObjectHandle)?;
1706            Ok(())
1707        }
1708        _ => Err(FigureError::InvalidPlotObjectHandle),
1709    }
1710}
1711
1712pub fn update_stem_plot(
1713    figure_handle: FigureHandle,
1714    plot_index: usize,
1715    updater: impl FnOnce(&mut runmat_plot::plots::StemPlot),
1716) -> Result<(), FigureError> {
1717    let mut reg = registry();
1718    let state = get_state_mut(&mut reg, figure_handle);
1719    let plot = state
1720        .figure
1721        .get_plot_mut(plot_index)
1722        .ok_or(FigureError::InvalidPlotObjectHandle)?;
1723    match plot {
1724        runmat_plot::plots::figure::PlotElement::Stem(stem) => {
1725            updater(stem);
1726            Ok(())
1727        }
1728        _ => Err(FigureError::InvalidPlotObjectHandle),
1729    }
1730}
1731
1732pub fn update_quiver_plot(
1733    figure_handle: FigureHandle,
1734    plot_index: usize,
1735    updater: impl FnOnce(&mut runmat_plot::plots::QuiverPlot),
1736) -> Result<(), FigureError> {
1737    let mut reg = registry();
1738    let state = get_state_mut(&mut reg, figure_handle);
1739    let plot = state
1740        .figure
1741        .get_plot_mut(plot_index)
1742        .ok_or(FigureError::InvalidPlotObjectHandle)?;
1743    match plot {
1744        runmat_plot::plots::figure::PlotElement::Quiver(quiver) => {
1745            updater(quiver);
1746            Ok(())
1747        }
1748        _ => Err(FigureError::InvalidPlotObjectHandle),
1749    }
1750}
1751
1752pub fn update_image_plot(
1753    figure_handle: FigureHandle,
1754    plot_index: usize,
1755    updater: impl FnOnce(&mut runmat_plot::plots::SurfacePlot),
1756) -> Result<(), FigureError> {
1757    let mut reg = registry();
1758    let state = get_state_mut(&mut reg, figure_handle);
1759    let plot = state
1760        .figure
1761        .get_plot_mut(plot_index)
1762        .ok_or(FigureError::InvalidPlotObjectHandle)?;
1763    match plot {
1764        runmat_plot::plots::figure::PlotElement::Surface(surface) if surface.image_mode => {
1765            updater(surface);
1766            Ok(())
1767        }
1768        _ => Err(FigureError::InvalidPlotObjectHandle),
1769    }
1770}
1771
1772pub fn update_area_plot(
1773    figure_handle: FigureHandle,
1774    plot_index: usize,
1775    updater: impl FnOnce(&mut runmat_plot::plots::AreaPlot),
1776) -> Result<(), FigureError> {
1777    let mut reg = registry();
1778    let state = get_state_mut(&mut reg, figure_handle);
1779    let plot = state
1780        .figure
1781        .get_plot_mut(plot_index)
1782        .ok_or(FigureError::InvalidPlotObjectHandle)?;
1783    match plot {
1784        runmat_plot::plots::figure::PlotElement::Area(area) => {
1785            updater(area);
1786            Ok(())
1787        }
1788        _ => Err(FigureError::InvalidPlotObjectHandle),
1789    }
1790}
1791
1792pub fn update_plot_element(
1793    figure_handle: FigureHandle,
1794    plot_index: usize,
1795    updater: impl FnOnce(&mut runmat_plot::plots::figure::PlotElement),
1796) -> Result<(), FigureError> {
1797    let mut reg = registry();
1798    let state = get_state_mut(&mut reg, figure_handle);
1799    let plot = state
1800        .figure
1801        .get_plot_mut(plot_index)
1802        .ok_or(FigureError::InvalidPlotObjectHandle)?;
1803    updater(plot);
1804    Ok(())
1805}
1806
1807fn purge_plot_children_for_figure(reg: &mut PlotRegistry, handle: FigureHandle) {
1808    reg.plot_children.retain(|_, state| match state {
1809        PlotChildHandleState::Histogram(hist) => hist.figure != handle,
1810        PlotChildHandleState::Line(plot)
1811        | PlotChildHandleState::Scatter(plot)
1812        | PlotChildHandleState::Bar(plot)
1813        | PlotChildHandleState::Stairs(plot)
1814        | PlotChildHandleState::Surface(plot)
1815        | PlotChildHandleState::Patch(plot)
1816        | PlotChildHandleState::Line3(plot)
1817        | PlotChildHandleState::Scatter3(plot)
1818        | PlotChildHandleState::Contour(plot)
1819        | PlotChildHandleState::ContourFill(plot)
1820        | PlotChildHandleState::ReferenceLine(plot)
1821        | PlotChildHandleState::Pie(plot) => plot.figure != handle,
1822        PlotChildHandleState::Stem(stem) => stem.figure != handle,
1823        PlotChildHandleState::ErrorBar(err) => err.figure != handle,
1824        PlotChildHandleState::Quiver(quiver) => quiver.figure != handle,
1825        PlotChildHandleState::Image(image) => image.figure != handle,
1826        PlotChildHandleState::Heatmap(heatmap) => heatmap.figure != handle,
1827        PlotChildHandleState::Area(area) => area.figure != handle,
1828        PlotChildHandleState::Text(text) => text.figure != handle,
1829    });
1830}
1831
1832fn purge_plot_children_for_axes(reg: &mut PlotRegistry, handle: FigureHandle, axes_index: usize) {
1833    reg.plot_children.retain(|_, state| match state {
1834        PlotChildHandleState::Histogram(hist) => {
1835            !(hist.figure == handle && hist.axes_index == axes_index)
1836        }
1837        PlotChildHandleState::Line(plot)
1838        | PlotChildHandleState::Scatter(plot)
1839        | PlotChildHandleState::Bar(plot)
1840        | PlotChildHandleState::Stairs(plot)
1841        | PlotChildHandleState::Surface(plot)
1842        | PlotChildHandleState::Patch(plot)
1843        | PlotChildHandleState::Line3(plot)
1844        | PlotChildHandleState::Scatter3(plot)
1845        | PlotChildHandleState::Contour(plot)
1846        | PlotChildHandleState::ContourFill(plot)
1847        | PlotChildHandleState::ReferenceLine(plot)
1848        | PlotChildHandleState::Pie(plot) => {
1849            !(plot.figure == handle && plot.axes_index == axes_index)
1850        }
1851        PlotChildHandleState::Stem(stem) => {
1852            !(stem.figure == handle && stem.axes_index == axes_index)
1853        }
1854        PlotChildHandleState::ErrorBar(err) => {
1855            !(err.figure == handle && err.axes_index == axes_index)
1856        }
1857        PlotChildHandleState::Quiver(quiver) => {
1858            !(quiver.figure == handle && quiver.axes_index == axes_index)
1859        }
1860        PlotChildHandleState::Image(image) => {
1861            !(image.figure == handle && image.axes_index == axes_index)
1862        }
1863        PlotChildHandleState::Heatmap(heatmap) => {
1864            !(heatmap.figure == handle && heatmap.axes_index == axes_index)
1865        }
1866        PlotChildHandleState::Area(area) => {
1867            !(area.figure == handle && area.axes_index == axes_index)
1868        }
1869        PlotChildHandleState::Text(text) => {
1870            !(text.figure == handle && text.axes_index == axes_index)
1871        }
1872    });
1873}
1874
1875pub fn decode_axes_handle(value: f64) -> Result<(FigureHandle, usize), FigureError> {
1876    if !value.is_finite() || value <= 0.0 {
1877        return Err(FigureError::InvalidAxesHandle);
1878    }
1879    let encoded = value.round() as u64;
1880    let figure_id = encoded >> AXES_INDEX_BITS;
1881    if figure_id == 0 {
1882        return Err(FigureError::InvalidAxesHandle);
1883    }
1884    let axes_index = (encoded & AXES_INDEX_MASK) as usize;
1885    Ok((FigureHandle::from(figure_id as u32), axes_index))
1886}
1887
1888#[cfg(not(target_arch = "wasm32"))]
1889fn registry() -> PlotRegistryGuard<'static> {
1890    #[cfg(test)]
1891    let test_lock = TEST_PLOT_OUTER_LOCK_HELD.with(|flag| {
1892        if flag.get() {
1893            None
1894        } else {
1895            Some(
1896                TEST_PLOT_REGISTRY_LOCK
1897                    .lock()
1898                    .unwrap_or_else(|e| e.into_inner()),
1899            )
1900        }
1901    });
1902    let guard = REGISTRY
1903        .get_or_init(|| Mutex::new(PlotRegistry::default()))
1904        .lock()
1905        .expect("plot registry poisoned");
1906    #[cfg(test)]
1907    {
1908        PlotRegistryGuard::new(guard, test_lock)
1909    }
1910    #[cfg(not(test))]
1911    {
1912        PlotRegistryGuard::new(guard)
1913    }
1914}
1915
1916#[cfg(target_arch = "wasm32")]
1917fn registry() -> PlotRegistryGuard<'static> {
1918    REGISTRY.with(|cell| {
1919        let guard = cell.borrow_mut();
1920        // SAFETY: the thread-local RefCell lives for the program lifetime and the borrow
1921        // guard is dropped when PlotRegistryGuard is dropped, so extending the lifetime
1922        // to 'static is sound.
1923        let guard_static: std::cell::RefMut<'static, PlotRegistry> =
1924            unsafe { std::mem::transmute::<std::cell::RefMut<'_, PlotRegistry>, _>(guard) };
1925        #[cfg(test)]
1926        {
1927            let test_lock = TEST_PLOT_OUTER_LOCK_HELD.with(|flag| {
1928                if flag.get() {
1929                    None
1930                } else {
1931                    Some(
1932                        TEST_PLOT_REGISTRY_LOCK
1933                            .lock()
1934                            .unwrap_or_else(|e| e.into_inner()),
1935                    )
1936                }
1937            });
1938            PlotRegistryGuard::new(guard_static, test_lock)
1939        }
1940        #[cfg(not(test))]
1941        {
1942            PlotRegistryGuard::new(guard_static)
1943        }
1944    })
1945}
1946
1947fn get_state_mut(registry: &mut PlotRegistry, handle: FigureHandle) -> &mut FigureState {
1948    registry
1949        .figures
1950        .entry(handle)
1951        .or_insert_with(|| FigureState::new(handle))
1952}
1953
1954fn observer_registry() -> &'static FigureObserverRegistry {
1955    FIGURE_OBSERVERS.get_or_init(FigureObserverRegistry::new)
1956}
1957
1958pub fn install_figure_observer(observer: Arc<FigureObserver>) -> BuiltinResult<()> {
1959    observer_registry().install(observer);
1960    Ok(())
1961}
1962
1963fn notify_event<'a>(view: FigureEventView<'a>) {
1964    note_recent_figure(view.handle);
1965    if let Some(registry) = FIGURE_OBSERVERS.get() {
1966        if registry.is_empty() {
1967            return;
1968        }
1969        registry.notify(view);
1970    }
1971}
1972
1973fn notify_with_figure(handle: FigureHandle, figure: &Figure, kind: FigureEventKind) {
1974    notify_event(FigureEventView {
1975        handle,
1976        kind,
1977        revision: current_figure_revision(handle),
1978        figure: Some(figure),
1979    });
1980}
1981
1982fn notify_without_figure(handle: FigureHandle, kind: FigureEventKind) {
1983    notify_event(FigureEventView {
1984        handle,
1985        kind,
1986        revision: current_figure_revision(handle),
1987        figure: None,
1988    });
1989}
1990
1991fn note_recent_figure(handle: FigureHandle) {
1992    RECENT_FIGURES.with(|set| {
1993        set.borrow_mut().insert(handle);
1994    });
1995}
1996
1997pub fn record_recent_figure(handle: FigureHandle) {
1998    note_recent_figure(handle);
1999}
2000
2001pub fn reset_recent_figures() {
2002    RECENT_FIGURES.with(|set| set.borrow_mut().clear());
2003}
2004
2005pub fn reset_plot_state() {
2006    {
2007        let mut reg = registry();
2008        *reg = PlotRegistry::default();
2009    }
2010    reset_recent_figures();
2011}
2012
2013pub fn take_recent_figures() -> Vec<FigureHandle> {
2014    RECENT_FIGURES.with(|set| set.borrow_mut().drain().collect())
2015}
2016
2017pub fn select_figure(handle: FigureHandle) {
2018    let mut reg = registry();
2019    reg.current = handle;
2020    let maybe_new = match reg.figures.entry(handle) {
2021        Entry::Occupied(entry) => {
2022            let _ = entry.into_mut();
2023            None
2024        }
2025        Entry::Vacant(vacant) => {
2026            let state = vacant.insert(FigureState::new(handle));
2027            Some(state.figure.clone())
2028        }
2029    };
2030    drop(reg);
2031    if let Some(figure_clone) = maybe_new {
2032        notify_with_figure(handle, &figure_clone, FigureEventKind::Created);
2033    }
2034}
2035
2036pub fn new_figure_handle() -> FigureHandle {
2037    let mut reg = registry();
2038    let handle = reg.next_handle;
2039    reg.next_handle = reg.next_handle.next();
2040    reg.current = handle;
2041    let figure_clone = {
2042        let state = get_state_mut(&mut reg, handle);
2043        state.figure.clone()
2044    };
2045    drop(reg);
2046    notify_with_figure(handle, &figure_clone, FigureEventKind::Created);
2047    handle
2048}
2049
2050pub fn current_figure_handle() -> FigureHandle {
2051    registry().current
2052}
2053
2054pub fn current_axes_state() -> FigureAxesState {
2055    let mut reg = registry();
2056    let handle = reg.current;
2057    // Ensure a default figure exists even if nothing has rendered yet (common on wasm/web).
2058    let state = get_state_mut(&mut reg, handle);
2059    FigureAxesState {
2060        handle,
2061        rows: state.figure.axes_rows.max(1),
2062        cols: state.figure.axes_cols.max(1),
2063        active_index: state.active_axes,
2064    }
2065}
2066
2067pub fn axes_handle_exists(handle: FigureHandle, axes_index: usize) -> bool {
2068    let mut reg = registry();
2069    let state = get_state_mut(&mut reg, handle);
2070    let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
2071    axes_index < total_axes
2072}
2073
2074pub fn figure_handle_exists(handle: FigureHandle) -> bool {
2075    let reg = registry();
2076    reg.figures.contains_key(&handle)
2077}
2078
2079pub fn axes_metadata_snapshot(
2080    handle: FigureHandle,
2081    axes_index: usize,
2082) -> Result<runmat_plot::plots::AxesMetadata, FigureError> {
2083    let mut reg = registry();
2084    let state = get_state_mut(&mut reg, handle);
2085    let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
2086    if axes_index >= total_axes {
2087        return Err(FigureError::InvalidSubplotIndex {
2088            rows: state.figure.axes_rows.max(1),
2089            cols: state.figure.axes_cols.max(1),
2090            index: axes_index,
2091        });
2092    }
2093    state
2094        .figure
2095        .axes_metadata(axes_index)
2096        .cloned()
2097        .ok_or(FigureError::InvalidAxesHandle)
2098}
2099
2100pub fn axes_state_snapshot(
2101    handle: FigureHandle,
2102    axes_index: usize,
2103) -> Result<FigureAxesState, FigureError> {
2104    let mut reg = registry();
2105    let state = get_state_mut(&mut reg, handle);
2106    let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
2107    if axes_index >= total_axes {
2108        return Err(FigureError::InvalidSubplotIndex {
2109            rows: state.figure.axes_rows.max(1),
2110            cols: state.figure.axes_cols.max(1),
2111            index: axes_index,
2112        });
2113    }
2114    Ok(FigureAxesState {
2115        handle,
2116        rows: state.figure.axes_rows.max(1),
2117        cols: state.figure.axes_cols.max(1),
2118        active_index: axes_index,
2119    })
2120}
2121
2122pub fn current_axes_handle_for_figure(handle: FigureHandle) -> Result<f64, FigureError> {
2123    let mut reg = registry();
2124    let state = get_state_mut(&mut reg, handle);
2125    Ok(encode_axes_handle(handle, state.active_axes))
2126}
2127
2128pub fn axes_handles_for_figure(handle: FigureHandle) -> Result<Vec<f64>, FigureError> {
2129    let mut reg = registry();
2130    let state = get_state_mut(&mut reg, handle);
2131    let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
2132    Ok((0..total_axes)
2133        .map(|idx| encode_axes_handle(handle, idx))
2134        .collect())
2135}
2136
2137pub fn select_axes_for_figure(handle: FigureHandle, axes_index: usize) -> Result<(), FigureError> {
2138    let mut reg = registry();
2139    let state = get_state_mut(&mut reg, handle);
2140    let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
2141    if axes_index >= total_axes {
2142        return Err(FigureError::InvalidSubplotIndex {
2143            rows: state.figure.axes_rows.max(1),
2144            cols: state.figure.axes_cols.max(1),
2145            index: axes_index,
2146        });
2147    }
2148    reg.current = handle;
2149    let state = get_state_mut(&mut reg, handle);
2150    state.active_axes = axes_index;
2151    state.figure.set_active_axes_index(axes_index);
2152    Ok(())
2153}
2154
2155fn with_axes_target_mut<R>(
2156    handle: FigureHandle,
2157    axes_index: usize,
2158    f: impl FnOnce(&mut FigureState) -> R,
2159) -> Result<(R, Figure), FigureError> {
2160    let mut reg = registry();
2161    let state = get_state_mut(&mut reg, handle);
2162    let total_axes = state.figure.axes_rows.max(1) * state.figure.axes_cols.max(1);
2163    if axes_index >= total_axes {
2164        return Err(FigureError::InvalidSubplotIndex {
2165            rows: state.figure.axes_rows.max(1),
2166            cols: state.figure.axes_cols.max(1),
2167            index: axes_index,
2168        });
2169    }
2170    state.active_axes = axes_index;
2171    state.figure.set_active_axes_index(axes_index);
2172    let result = f(state);
2173    state.revision = state.revision.wrapping_add(1);
2174    Ok((result, state.figure.clone()))
2175}
2176
2177fn with_figure_mut<R>(
2178    handle: FigureHandle,
2179    f: impl FnOnce(&mut FigureState) -> R,
2180) -> Result<(R, Figure), FigureError> {
2181    let mut reg = registry();
2182    let state = get_state_mut(&mut reg, handle);
2183    let result = f(state);
2184    state.revision = state.revision.wrapping_add(1);
2185    Ok((result, state.figure.clone()))
2186}
2187
2188pub fn current_hold_enabled() -> bool {
2189    let mut reg = registry();
2190    let handle = reg.current;
2191    // Ensure a default figure exists even if nothing has rendered yet (common on wasm/web).
2192    let state = get_state_mut(&mut reg, handle);
2193    *state
2194        .hold_per_axes
2195        .get(&state.active_axes)
2196        .unwrap_or(&false)
2197}
2198
2199/// Reset hold state for all figures/axes.
2200///
2201/// In the IDE, we want re-running code to behave like a fresh plotting run unless the code
2202/// explicitly enables `hold on` again. Without this, a prior `hold on` will cause subsequent
2203/// runs to keep appending to the same axes (surprising for typical "Run" workflows).
2204pub fn reset_hold_state_for_run() {
2205    let mut reg = registry();
2206    for state in reg.figures.values_mut() {
2207        state.hold_per_axes.clear();
2208    }
2209}
2210
2211pub fn figure_handles() -> Vec<FigureHandle> {
2212    let reg = registry();
2213    reg.figures.keys().copied().collect()
2214}
2215
2216pub fn clone_figure(handle: FigureHandle) -> Option<Figure> {
2217    let reg = registry();
2218    reg.figures.get(&handle).map(|state| state.figure.clone())
2219}
2220
2221pub fn figure_has_sg_title(handle: FigureHandle) -> bool {
2222    let reg = registry();
2223    reg.figures
2224        .get(&handle)
2225        .map(|state| state.figure.sg_title.is_some())
2226        .unwrap_or(false)
2227}
2228
2229pub fn import_figure(figure: Figure) -> FigureHandle {
2230    let mut reg = registry();
2231    let handle = reg.next_handle;
2232    reg.next_handle = reg.next_handle.next();
2233    reg.current = handle;
2234    let figure_clone = figure.clone();
2235    reg.figures.insert(
2236        handle,
2237        FigureState {
2238            figure,
2239            ..FigureState::new(handle)
2240        },
2241    );
2242    drop(reg);
2243    notify_with_figure(handle, &figure_clone, FigureEventKind::Created);
2244    handle
2245}
2246
2247pub fn clear_figure(target: Option<FigureHandle>) -> Result<FigureHandle, FigureError> {
2248    let mut reg = registry();
2249    let handle = target.unwrap_or(reg.current);
2250    {
2251        let state = reg
2252            .figures
2253            .get_mut(&handle)
2254            .ok_or(FigureError::InvalidHandle(handle.as_u32()))?;
2255        *state = FigureState::new(handle);
2256    }
2257    purge_plot_children_for_figure(&mut reg, handle);
2258    let figure_clone = reg
2259        .figures
2260        .get(&handle)
2261        .expect("figure exists")
2262        .figure
2263        .clone();
2264    drop(reg);
2265    notify_with_figure(handle, &figure_clone, FigureEventKind::Cleared);
2266    Ok(handle)
2267}
2268
2269pub fn close_figure(target: Option<FigureHandle>) -> Result<FigureHandle, FigureError> {
2270    let mut reg = registry();
2271    let handle = target.unwrap_or(reg.current);
2272    let existed = reg.figures.remove(&handle);
2273    if existed.is_none() {
2274        return Err(FigureError::InvalidHandle(handle.as_u32()));
2275    }
2276    purge_plot_children_for_figure(&mut reg, handle);
2277
2278    if reg.current == handle {
2279        if let Some((&next_handle, _)) = reg.figures.iter().next() {
2280            reg.current = next_handle;
2281        } else {
2282            let default = FigureHandle::default();
2283            reg.current = default;
2284            reg.next_handle = default.next();
2285            drop(reg);
2286            notify_without_figure(handle, FigureEventKind::Closed);
2287            return Ok(handle);
2288        }
2289    }
2290
2291    drop(reg);
2292    notify_without_figure(handle, FigureEventKind::Closed);
2293    Ok(handle)
2294}
2295
2296#[derive(Clone)]
2297pub struct PlotRenderOptions<'a> {
2298    pub title: &'a str,
2299    pub x_label: &'a str,
2300    pub y_label: &'a str,
2301    pub grid: bool,
2302    pub axis_equal: bool,
2303}
2304
2305impl<'a> Default for PlotRenderOptions<'a> {
2306    fn default() -> Self {
2307        Self {
2308            title: "",
2309            x_label: "X",
2310            y_label: "Y",
2311            grid: true,
2312            axis_equal: false,
2313        }
2314    }
2315}
2316
2317pub enum HoldMode {
2318    On,
2319    Off,
2320    Toggle,
2321}
2322
2323pub fn set_hold(mode: HoldMode) -> bool {
2324    let mut reg = registry();
2325    let handle = reg.current;
2326    let state = get_state_mut(&mut reg, handle);
2327    let current = state.hold();
2328    let new_value = match mode {
2329        HoldMode::On => true,
2330        HoldMode::Off => false,
2331        HoldMode::Toggle => !current,
2332    };
2333    state.set_hold(new_value);
2334    new_value
2335}
2336
2337pub fn configure_subplot(rows: usize, cols: usize, index: usize) -> Result<(), FigureError> {
2338    if rows == 0 || cols == 0 {
2339        return Err(FigureError::InvalidSubplotGrid { rows, cols });
2340    }
2341    let total_axes = rows
2342        .checked_mul(cols)
2343        .ok_or(FigureError::InvalidSubplotGrid { rows, cols })?;
2344    if index >= total_axes {
2345        return Err(FigureError::InvalidSubplotIndex { rows, cols, index });
2346    }
2347    let mut reg = registry();
2348    let handle = reg.current;
2349    let state = get_state_mut(&mut reg, handle);
2350    state.figure.set_subplot_grid(rows, cols);
2351    state.active_axes = index;
2352    state.figure.set_active_axes_index(index);
2353    Ok(())
2354}
2355
2356pub fn render_active_plot<F>(
2357    builtin: &'static str,
2358    opts: PlotRenderOptions<'_>,
2359    mut apply: F,
2360) -> BuiltinResult<String>
2361where
2362    F: FnMut(&mut Figure, usize) -> BuiltinResult<()>,
2363{
2364    let rendering_disabled = interactive_rendering_disabled();
2365    let host_managed_rendering = host_managed_rendering_enabled();
2366    let (handle, figure_clone) = {
2367        let mut reg = registry();
2368        let handle = reg.current;
2369        let axes_index = { get_state_mut(&mut reg, handle).active_axes };
2370        let should_clear = { !get_state_mut(&mut reg, handle).hold() };
2371        {
2372            let state = get_state_mut(&mut reg, handle);
2373            state.figure.set_active_axes_index(axes_index);
2374            if should_clear {
2375                state.figure.clear_axes(axes_index);
2376                state.figure.set_axes_kind(axes_index, AxesKind::Cartesian);
2377                state.figure.set_axes_limits(axes_index, None, None);
2378                state.figure.set_axes_z_limits(axes_index, None);
2379                state.reset_cycle(axes_index);
2380            }
2381        }
2382        if should_clear {
2383            purge_plot_children_for_axes(&mut reg, handle, axes_index);
2384        }
2385        {
2386            let state = get_state_mut(&mut reg, handle);
2387            if !opts.title.is_empty() {
2388                state.figure.set_axes_title(axes_index, opts.title);
2389            }
2390            if !opts.x_label.is_empty() || !opts.y_label.is_empty() {
2391                state
2392                    .figure
2393                    .set_axes_labels(axes_index, opts.x_label, opts.y_label);
2394            }
2395            state.figure.set_grid(opts.grid);
2396            state.figure.set_axis_equal(opts.axis_equal);
2397
2398            let _axes_context = AxesContextGuard::install(state, axes_index);
2399            apply(&mut state.figure, axes_index)
2400                .map_err(|flow| map_control_flow_with_builtin(flow, builtin))?;
2401
2402            // Increment revision after a successful mutation so surfaces can avoid
2403            // re-rendering unchanged figures when "presenting" an already-loaded handle.
2404            state.revision = state.revision.wrapping_add(1);
2405        }
2406        let figure_clone = reg
2407            .figures
2408            .get(&handle)
2409            .expect("figure exists")
2410            .figure
2411            .clone();
2412        (handle, figure_clone)
2413    };
2414    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2415
2416    present_figure_update_with_options(
2417        builtin,
2418        handle,
2419        figure_clone,
2420        rendering_disabled,
2421        host_managed_rendering,
2422    )
2423}
2424
2425pub fn append_active_plot<F>(
2426    builtin: &'static str,
2427    opts: PlotRenderOptions<'_>,
2428    mut apply: F,
2429) -> BuiltinResult<String>
2430where
2431    F: FnMut(&mut Figure, usize) -> BuiltinResult<()>,
2432{
2433    let rendering_disabled = interactive_rendering_disabled();
2434    let host_managed_rendering = host_managed_rendering_enabled();
2435    let (handle, figure_clone) = {
2436        let mut reg = registry();
2437        let handle = reg.current;
2438        let axes_index = { get_state_mut(&mut reg, handle).active_axes };
2439        {
2440            let state = get_state_mut(&mut reg, handle);
2441            state.figure.set_active_axes_index(axes_index);
2442            if !opts.title.is_empty() {
2443                state.figure.set_axes_title(axes_index, opts.title);
2444            }
2445            if !opts.x_label.is_empty() || !opts.y_label.is_empty() {
2446                state
2447                    .figure
2448                    .set_axes_labels(axes_index, opts.x_label, opts.y_label);
2449            }
2450            state.figure.set_grid(opts.grid);
2451            state.figure.set_axis_equal(opts.axis_equal);
2452
2453            let _axes_context = AxesContextGuard::install(state, axes_index);
2454            apply(&mut state.figure, axes_index)
2455                .map_err(|flow| map_control_flow_with_builtin(flow, builtin))?;
2456            state.revision = state.revision.wrapping_add(1);
2457        }
2458        let figure_clone = reg
2459            .figures
2460            .get(&handle)
2461            .expect("figure exists")
2462            .figure
2463            .clone();
2464        (handle, figure_clone)
2465    };
2466    notify_with_figure(handle, &figure_clone, FigureEventKind::Updated);
2467
2468    present_figure_update_with_options(
2469        builtin,
2470        handle,
2471        figure_clone,
2472        rendering_disabled,
2473        host_managed_rendering,
2474    )
2475}
2476
2477pub fn present_figure_update(
2478    builtin: &'static str,
2479    handle: FigureHandle,
2480    figure_clone: Figure,
2481) -> BuiltinResult<String> {
2482    present_figure_update_with_options(
2483        builtin,
2484        handle,
2485        figure_clone,
2486        interactive_rendering_disabled(),
2487        host_managed_rendering_enabled(),
2488    )
2489}
2490
2491fn present_figure_update_with_options(
2492    builtin: &'static str,
2493    handle: FigureHandle,
2494    figure_clone: Figure,
2495    rendering_disabled: bool,
2496    host_managed_rendering: bool,
2497) -> BuiltinResult<String> {
2498    let updated = || format!("Figure {} updated", handle.as_u32());
2499    if !figure_clone.visible {
2500        #[cfg(all(
2501            feature = "gui",
2502            not(all(target_arch = "wasm32", feature = "plot-web"))
2503        ))]
2504        {
2505            if !rendering_disabled && !host_managed_rendering {
2506                let rendered = render_figure(handle, figure_clone)
2507                    .map_err(|flow| map_control_flow_with_builtin(flow, builtin))?;
2508                return Ok(format!("{}: {rendered}", updated()));
2509            }
2510        }
2511        return Ok(updated());
2512    }
2513
2514    if rendering_disabled {
2515        if host_managed_rendering {
2516            return Ok(updated());
2517        }
2518        return Err(plotting_error(builtin, ERR_PLOTTING_UNAVAILABLE));
2519    }
2520
2521    if host_managed_rendering {
2522        return Ok(updated());
2523    }
2524
2525    // On Web/WASM we deliberately decouple "mutate figure state" from "present pixels".
2526    // The host coalesces figure events and presents on a frame cadence, and `drawnow()` /
2527    // `pause()` provide explicit "flush" boundaries for scripts.
2528    #[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
2529    {
2530        let _ = figure_clone;
2531        Ok(updated())
2532    }
2533
2534    #[cfg(not(all(target_arch = "wasm32", feature = "plot-web")))]
2535    {
2536        let rendered = render_figure(handle, figure_clone)
2537            .map_err(|flow| map_control_flow_with_builtin(flow, builtin))?;
2538        Ok(format!("{}: {rendered}", updated()))
2539    }
2540}
2541
2542/// Monotonic revision counter that increments on each successful mutation of the figure.
2543/// Used by web surface presentation logic to avoid redundant `render_figure` calls when
2544/// a surface is already up-to-date for a handle.
2545pub fn current_figure_revision(handle: FigureHandle) -> Option<u64> {
2546    let reg = registry();
2547    reg.figures.get(&handle).map(|state| state.revision)
2548}
2549
2550fn interactive_rendering_disabled() -> bool {
2551    std::env::var_os("RUNMAT_DISABLE_INTERACTIVE_PLOTS").is_some()
2552}
2553
2554fn host_managed_rendering_enabled() -> bool {
2555    std::env::var_os("RUNMAT_HOST_MANAGED_PLOTS").is_some()
2556}
2557
2558#[cfg(test)]
2559pub(crate) fn disable_rendering_for_tests() {
2560    set_plot_test_env_vars();
2561}
2562
2563pub fn set_line_style_order_for_axes(axes_index: usize, order: &[LineStyle]) {
2564    if with_active_style_cycle(axes_index, |cycle| cycle.set_order(order)).is_some() {
2565        return;
2566    }
2567    let mut reg = registry();
2568    let handle = reg.current;
2569    let state = get_state_mut(&mut reg, handle);
2570    state.cycle_for_axes_mut(axes_index).set_order(order);
2571}
2572
2573pub fn next_line_style_for_axes(axes_index: usize) -> LineStyle {
2574    if let Some(style) = with_active_style_cycle(axes_index, |cycle| cycle.next()) {
2575        return style;
2576    }
2577    let mut reg = registry();
2578    let handle = reg.current;
2579    let state = get_state_mut(&mut reg, handle);
2580    state.cycle_for_axes_mut(axes_index).next()
2581}
2582
2583pub fn line_color_for_series_index(series_index: usize) -> Vec4 {
2584    let theme = current_plot_theme_config().build_theme();
2585    theme.get_data_color(series_index)
2586}
2587
2588pub fn next_line_color_for_axes(axes_index: usize) -> Vec4 {
2589    if let Some(color) = with_active_color_cycle(axes_index, |cycle| cycle.next()) {
2590        return color;
2591    }
2592    let mut reg = registry();
2593    let handle = reg.current;
2594    let state = get_state_mut(&mut reg, handle);
2595    state.color_cycle_for_axes_mut(axes_index).next()
2596}
2597
2598#[cfg(test)]
2599mod tests {
2600    use super::*;
2601    use crate::builtins::plotting::tests::ensure_plot_test_env;
2602
2603    #[cfg(test)]
2604    pub(crate) fn reset_for_tests() {
2605        let mut reg = registry();
2606        reg.figures.clear();
2607        reg.current = FigureHandle::default();
2608        reg.next_handle = FigureHandle::default().next();
2609    }
2610
2611    #[test]
2612    fn closing_last_figure_leaves_no_visible_figures() {
2613        let _guard = lock_plot_test_registry();
2614        ensure_plot_test_env();
2615        reset_for_tests();
2616
2617        let handle = new_figure_handle();
2618        assert_eq!(figure_handles(), vec![handle]);
2619
2620        close_figure(Some(handle)).expect("close figure");
2621
2622        assert!(
2623            figure_handles().is_empty(),
2624            "closing the last figure should not recreate a default visible figure"
2625        );
2626    }
2627
2628    #[test]
2629    fn hidden_figure_update_does_not_require_interactive_renderer() {
2630        let _guard = lock_plot_test_registry();
2631        ensure_plot_test_env();
2632        reset_for_tests();
2633
2634        let handle = new_figure_handle();
2635        let mut figure = clone_figure(handle).expect("figure exists");
2636        figure.set_visible(false);
2637
2638        let result = present_figure_update_with_options("plot", handle, figure, true, false)
2639            .expect("hidden figure should not try to present");
2640        assert_eq!(result, format!("Figure {} updated", handle.as_u32()));
2641    }
2642
2643    #[cfg(all(
2644        feature = "gui",
2645        not(all(target_arch = "wasm32", feature = "plot-web"))
2646    ))]
2647    #[test]
2648    fn hidden_figure_update_uses_native_close_path_when_rendering_available() {
2649        let _guard = lock_plot_test_registry();
2650        ensure_plot_test_env();
2651        reset_for_tests();
2652
2653        let handle = new_figure_handle();
2654        let mut figure = clone_figure(handle).expect("figure exists");
2655        figure.set_visible(false);
2656
2657        let result = present_figure_update_with_options("plot", handle, figure, false, false)
2658            .expect("hidden figure should request native close");
2659        assert_eq!(
2660            result,
2661            format!(
2662                "Figure {} updated: Figure {} is hidden",
2663                handle.as_u32(),
2664                handle.as_u32()
2665            )
2666        );
2667    }
2668
2669    #[test]
2670    fn hidden_figure_update_does_not_render_when_host_managed() {
2671        let _guard = lock_plot_test_registry();
2672        ensure_plot_test_env();
2673        reset_for_tests();
2674
2675        let handle = new_figure_handle();
2676        let mut figure = clone_figure(handle).expect("figure exists");
2677        figure.set_visible(false);
2678
2679        let result = present_figure_update_with_options("plot", handle, figure, false, true)
2680            .expect("hidden host-managed figure should not render directly");
2681        assert_eq!(result, format!("Figure {} updated", handle.as_u32()));
2682    }
2683
2684    #[test]
2685    fn toggle_minor_grid_uses_effective_inherited_state() {
2686        let _guard = lock_plot_test_registry();
2687        ensure_plot_test_env();
2688        reset_for_tests();
2689
2690        let handle = new_figure_handle();
2691        {
2692            let mut reg = registry();
2693            let state = get_state_mut(&mut reg, handle);
2694            state.figure.minor_grid_enabled = true;
2695            assert!(state.figure.minor_grid_enabled_for_axes(state.active_axes));
2696            assert!(
2697                !state
2698                    .figure
2699                    .axes_metadata(state.active_axes)
2700                    .expect("active axes metadata")
2701                    .minor_grid_explicit
2702            );
2703        }
2704
2705        assert!(!toggle_minor_grid());
2706
2707        let figure = clone_figure(handle).expect("figure exists");
2708        assert!(!figure.minor_grid_enabled_for_axes(0));
2709        let meta = figure.axes_metadata(0).expect("active axes metadata");
2710        assert!(meta.minor_grid_explicit);
2711        assert!(!meta.minor_grid_enabled);
2712    }
2713}