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