dear_implot/
context.rs

1use crate::{AxisFlags, PlotCond, XAxis, YAxis, sys};
2use dear_imgui_rs::{
3    Context as ImGuiContext, Ui, with_scratch_txt, with_scratch_txt_slice, with_scratch_txt_two,
4};
5use dear_imgui_sys as imgui_sys;
6use std::os::raw::c_char;
7use std::{cell::RefCell, rc::Rc};
8
9/// ImPlot context that manages the plotting state
10///
11/// This context is separate from the Dear ImGui context but works alongside it.
12/// You need both contexts to create plots.
13pub struct PlotContext {
14    raw: *mut sys::ImPlotContext,
15}
16
17impl PlotContext {
18    /// Try to create a new ImPlot context
19    ///
20    /// This should be called after creating the Dear ImGui context.
21    /// The ImPlot context will use the same Dear ImGui context internally.
22    pub fn try_create(_imgui_ctx: &ImGuiContext) -> dear_imgui_rs::ImGuiResult<Self> {
23        // Bind ImPlot to the current Dear ImGui context before creating.
24        // On some toolchains/platforms, not setting this can lead to crashes
25        // if ImPlot initialization queries ImGui state during CreateContext.
26        unsafe {
27            sys::ImPlot_SetImGuiContext(imgui_sys::igGetCurrentContext());
28        }
29
30        let raw = unsafe { sys::ImPlot_CreateContext() };
31        if raw.is_null() {
32            return Err(dear_imgui_rs::ImGuiError::context_creation(
33                "ImPlot_CreateContext returned null",
34            ));
35        }
36
37        // Ensure the newly created context is current (defensive, CreateContext should do this).
38        unsafe {
39            sys::ImPlot_SetCurrentContext(raw);
40        }
41
42        Ok(Self { raw })
43    }
44
45    /// Create a new ImPlot context (panics on error)
46    pub fn create(imgui_ctx: &ImGuiContext) -> Self {
47        Self::try_create(imgui_ctx).expect("Failed to create ImPlot context")
48    }
49
50    /// Get the current ImPlot context
51    ///
52    /// Returns None if no context is current
53    pub fn current() -> Option<Self> {
54        let raw = unsafe { sys::ImPlot_GetCurrentContext() };
55        if raw.is_null() {
56            None
57        } else {
58            Some(Self { raw })
59        }
60    }
61
62    /// Set this context as the current ImPlot context
63    pub fn set_as_current(&self) {
64        unsafe {
65            sys::ImPlot_SetCurrentContext(self.raw);
66        }
67    }
68
69    /// Get a PlotUi for creating plots
70    ///
71    /// This borrows both the ImPlot context and the Dear ImGui Ui,
72    /// ensuring that plots can only be created when both are available.
73    pub fn get_plot_ui<'ui>(&'ui self, ui: &'ui Ui) -> PlotUi<'ui> {
74        PlotUi { context: self, ui }
75    }
76
77    /// Get the raw ImPlot context pointer
78    ///
79    /// # Safety
80    ///
81    /// The caller must ensure the pointer is used safely and not stored
82    /// beyond the lifetime of this context.
83    pub unsafe fn raw(&self) -> *mut sys::ImPlotContext {
84        self.raw
85    }
86}
87
88impl Drop for PlotContext {
89    fn drop(&mut self) {
90        if !self.raw.is_null() {
91            unsafe {
92                sys::ImPlot_DestroyContext(self.raw);
93            }
94        }
95    }
96}
97
98// ImPlot context is tied to Dear ImGui and not thread-safe to send/share.
99
100/// A temporary reference for building plots
101///
102/// This struct ensures that plots can only be created when both ImGui and ImPlot
103/// contexts are available and properly set up.
104pub struct PlotUi<'ui> {
105    #[allow(dead_code)]
106    context: &'ui PlotContext,
107    #[allow(dead_code)]
108    ui: &'ui Ui,
109}
110
111impl<'ui> PlotUi<'ui> {
112    /// Begin a new plot with the given title
113    ///
114    /// Returns a PlotToken if the plot was successfully started.
115    /// The plot will be automatically ended when the token is dropped.
116    pub fn begin_plot(&self, title: &str) -> Option<PlotToken<'_>> {
117        let size = sys::ImVec2_c { x: -1.0, y: 0.0 };
118        if title.contains('\0') {
119            return None;
120        }
121        let started = with_scratch_txt(title, |ptr| unsafe { sys::ImPlot_BeginPlot(ptr, size, 0) });
122
123        if started {
124            Some(PlotToken::new())
125        } else {
126            None
127        }
128    }
129
130    /// Begin a plot with custom size
131    pub fn begin_plot_with_size(&self, title: &str, size: [f32; 2]) -> Option<PlotToken<'_>> {
132        let plot_size = sys::ImVec2_c {
133            x: size[0],
134            y: size[1],
135        };
136        if title.contains('\0') {
137            return None;
138        }
139        let started = with_scratch_txt(title, |ptr| unsafe {
140            sys::ImPlot_BeginPlot(ptr, plot_size, 0)
141        });
142
143        if started {
144            Some(PlotToken::new())
145        } else {
146            None
147        }
148    }
149
150    /// Plot a line with the given label and data
151    ///
152    /// This is a convenience method that can be called within a plot.
153    pub fn plot_line(&self, label: &str, x_data: &[f64], y_data: &[f64]) {
154        if x_data.len() != y_data.len() {
155            return; // Data length mismatch
156        }
157        let count = match i32::try_from(x_data.len()) {
158            Ok(v) => v,
159            Err(_) => return,
160        };
161
162        let label = if label.contains('\0') { "" } else { label };
163        with_scratch_txt(label, |ptr| unsafe {
164            sys::ImPlot_PlotLine_doublePtrdoublePtr(
165                ptr,
166                x_data.as_ptr(),
167                y_data.as_ptr(),
168                count,
169                0,
170                0,
171                0,
172            );
173        })
174    }
175
176    /// Plot a scatter plot with the given label and data
177    pub fn plot_scatter(&self, label: &str, x_data: &[f64], y_data: &[f64]) {
178        if x_data.len() != y_data.len() {
179            return; // Data length mismatch
180        }
181        let count = match i32::try_from(x_data.len()) {
182            Ok(v) => v,
183            Err(_) => return,
184        };
185
186        let label = if label.contains('\0') { "" } else { label };
187        with_scratch_txt(label, |ptr| unsafe {
188            sys::ImPlot_PlotScatter_doublePtrdoublePtr(
189                ptr,
190                x_data.as_ptr(),
191                y_data.as_ptr(),
192                count,
193                0,
194                0,
195                0,
196            );
197        })
198    }
199
200    /// Check if the plot area is hovered
201    pub fn is_plot_hovered(&self) -> bool {
202        unsafe { sys::ImPlot_IsPlotHovered() }
203    }
204
205    /// Get the mouse position in plot coordinates
206    pub fn get_plot_mouse_pos(&self, y_axis: Option<crate::YAxisChoice>) -> sys::ImPlotPoint {
207        let y_axis_i32 = crate::y_axis_choice_option_to_i32(y_axis);
208        let y_axis = match y_axis_i32 {
209            0 => 3,
210            1 => 4,
211            2 => 5,
212            _ => 3,
213        };
214        unsafe { sys::ImPlot_GetPlotMousePos(0, y_axis) }
215    }
216
217    /// Get the mouse position in plot coordinates for specific axes
218    pub fn get_plot_mouse_pos_axes(&self, x_axis: XAxis, y_axis: YAxis) -> sys::ImPlotPoint {
219        unsafe { sys::ImPlot_GetPlotMousePos(x_axis as i32, y_axis as i32) }
220    }
221
222    /// Set current axes for subsequent plot submissions
223    pub fn set_axes(&self, x_axis: XAxis, y_axis: YAxis) {
224        unsafe { sys::ImPlot_SetAxes(x_axis as i32, y_axis as i32) }
225    }
226
227    /// Setup a specific X axis
228    pub fn setup_x_axis(&self, axis: XAxis, label: Option<&str>, flags: AxisFlags) {
229        let label = label.filter(|s| !s.contains('\0'));
230        match label {
231            Some(label) => with_scratch_txt(label, |ptr| unsafe {
232                sys::ImPlot_SetupAxis(
233                    axis as sys::ImAxis,
234                    ptr,
235                    flags.bits() as sys::ImPlotAxisFlags,
236                )
237            }),
238            None => unsafe {
239                sys::ImPlot_SetupAxis(
240                    axis as sys::ImAxis,
241                    std::ptr::null(),
242                    flags.bits() as sys::ImPlotAxisFlags,
243                )
244            },
245        }
246    }
247
248    /// Setup a specific Y axis
249    pub fn setup_y_axis(&self, axis: YAxis, label: Option<&str>, flags: AxisFlags) {
250        let label = label.filter(|s| !s.contains('\0'));
251        match label {
252            Some(label) => with_scratch_txt(label, |ptr| unsafe {
253                sys::ImPlot_SetupAxis(
254                    axis as sys::ImAxis,
255                    ptr,
256                    flags.bits() as sys::ImPlotAxisFlags,
257                )
258            }),
259            None => unsafe {
260                sys::ImPlot_SetupAxis(
261                    axis as sys::ImAxis,
262                    std::ptr::null(),
263                    flags.bits() as sys::ImPlotAxisFlags,
264                )
265            },
266        }
267    }
268
269    /// Setup axis limits for a specific X axis
270    pub fn setup_x_axis_limits(&self, axis: XAxis, min: f64, max: f64, cond: PlotCond) {
271        unsafe {
272            sys::ImPlot_SetupAxisLimits(axis as sys::ImAxis, min, max, cond as sys::ImPlotCond)
273        }
274    }
275
276    /// Setup axis limits for a specific Y axis
277    pub fn setup_y_axis_limits(&self, axis: YAxis, min: f64, max: f64, cond: PlotCond) {
278        unsafe {
279            sys::ImPlot_SetupAxisLimits(axis as sys::ImAxis, min, max, cond as sys::ImPlotCond)
280        }
281    }
282
283    /// Link an axis to external min/max values (live binding)
284    pub fn setup_axis_links(
285        &self,
286        axis: i32,
287        link_min: Option<&mut f64>,
288        link_max: Option<&mut f64>,
289    ) {
290        let pmin = link_min.map_or(std::ptr::null_mut(), |r| r as *mut f64);
291        let pmax = link_max.map_or(std::ptr::null_mut(), |r| r as *mut f64);
292        unsafe { sys::ImPlot_SetupAxisLinks(axis, pmin, pmax) }
293    }
294
295    /// Setup both axes labels/flags at once
296    pub fn setup_axes(
297        &self,
298        x_label: Option<&str>,
299        y_label: Option<&str>,
300        x_flags: AxisFlags,
301        y_flags: AxisFlags,
302    ) {
303        let x_label = x_label.filter(|s| !s.contains('\0'));
304        let y_label = y_label.filter(|s| !s.contains('\0'));
305
306        match (x_label, y_label) {
307            (Some(x_label), Some(y_label)) => {
308                with_scratch_txt_two(x_label, y_label, |xp, yp| unsafe {
309                    sys::ImPlot_SetupAxes(
310                        xp,
311                        yp,
312                        x_flags.bits() as sys::ImPlotAxisFlags,
313                        y_flags.bits() as sys::ImPlotAxisFlags,
314                    )
315                })
316            }
317            (Some(x_label), None) => with_scratch_txt(x_label, |xp| unsafe {
318                sys::ImPlot_SetupAxes(
319                    xp,
320                    std::ptr::null(),
321                    x_flags.bits() as sys::ImPlotAxisFlags,
322                    y_flags.bits() as sys::ImPlotAxisFlags,
323                )
324            }),
325            (None, Some(y_label)) => with_scratch_txt(y_label, |yp| unsafe {
326                sys::ImPlot_SetupAxes(
327                    std::ptr::null(),
328                    yp,
329                    x_flags.bits() as sys::ImPlotAxisFlags,
330                    y_flags.bits() as sys::ImPlotAxisFlags,
331                )
332            }),
333            (None, None) => unsafe {
334                sys::ImPlot_SetupAxes(
335                    std::ptr::null(),
336                    std::ptr::null(),
337                    x_flags.bits() as sys::ImPlotAxisFlags,
338                    y_flags.bits() as sys::ImPlotAxisFlags,
339                )
340            },
341        }
342    }
343
344    /// Setup axes limits (both) at once
345    pub fn setup_axes_limits(
346        &self,
347        x_min: f64,
348        x_max: f64,
349        y_min: f64,
350        y_max: f64,
351        cond: PlotCond,
352    ) {
353        unsafe { sys::ImPlot_SetupAxesLimits(x_min, x_max, y_min, y_max, cond as sys::ImPlotCond) }
354    }
355
356    /// Call after axis setup to finalize configuration
357    pub fn setup_finish(&self) {
358        unsafe { sys::ImPlot_SetupFinish() }
359    }
360
361    /// Set next frame limits for a specific axis
362    pub fn set_next_x_axis_limits(&self, axis: XAxis, min: f64, max: f64, cond: PlotCond) {
363        unsafe {
364            sys::ImPlot_SetNextAxisLimits(axis as sys::ImAxis, min, max, cond as sys::ImPlotCond)
365        }
366    }
367
368    /// Set next frame limits for a specific axis
369    pub fn set_next_y_axis_limits(&self, axis: YAxis, min: f64, max: f64, cond: PlotCond) {
370        unsafe {
371            sys::ImPlot_SetNextAxisLimits(axis as sys::ImAxis, min, max, cond as sys::ImPlotCond)
372        }
373    }
374
375    /// Link an axis to external min/max for next frame
376    pub fn set_next_axis_links(
377        &self,
378        axis: i32,
379        link_min: Option<&mut f64>,
380        link_max: Option<&mut f64>,
381    ) {
382        let pmin = link_min.map_or(std::ptr::null_mut(), |r| r as *mut f64);
383        let pmax = link_max.map_or(std::ptr::null_mut(), |r| r as *mut f64);
384        unsafe { sys::ImPlot_SetNextAxisLinks(axis, pmin, pmax) }
385    }
386
387    /// Set next frame limits for both axes
388    pub fn set_next_axes_limits(
389        &self,
390        x_min: f64,
391        x_max: f64,
392        y_min: f64,
393        y_max: f64,
394        cond: PlotCond,
395    ) {
396        unsafe {
397            sys::ImPlot_SetNextAxesLimits(x_min, x_max, y_min, y_max, cond as sys::ImPlotCond)
398        }
399    }
400
401    /// Fit next frame both axes
402    pub fn set_next_axes_to_fit(&self) {
403        unsafe { sys::ImPlot_SetNextAxesToFit() }
404    }
405
406    /// Fit next frame a specific axis (raw)
407    pub fn set_next_axis_to_fit(&self, axis: i32) {
408        unsafe { sys::ImPlot_SetNextAxisToFit(axis as sys::ImAxis) }
409    }
410
411    /// Fit next frame a specific X axis
412    pub fn set_next_x_axis_to_fit(&self, axis: XAxis) {
413        unsafe { sys::ImPlot_SetNextAxisToFit(axis as sys::ImAxis) }
414    }
415
416    /// Fit next frame a specific Y axis
417    pub fn set_next_y_axis_to_fit(&self, axis: YAxis) {
418        unsafe { sys::ImPlot_SetNextAxisToFit(axis as sys::ImAxis) }
419    }
420
421    /// Setup ticks with explicit positions and optional labels for an X axis.
422    ///
423    /// If `labels` is provided, it must have the same length as `values`.
424    pub fn setup_x_axis_ticks_positions(
425        &self,
426        axis: XAxis,
427        values: &[f64],
428        labels: Option<&[&str]>,
429        keep_default: bool,
430    ) {
431        let count = match i32::try_from(values.len()) {
432            Ok(v) => v,
433            Err(_) => return,
434        };
435        if let Some(labels) = labels {
436            if labels.len() != values.len() {
437                return;
438            }
439            let cleaned: Vec<&str> = labels
440                .iter()
441                .map(|&s| if s.contains('\0') { "" } else { s })
442                .collect();
443            with_scratch_txt_slice(&cleaned, |ptrs| unsafe {
444                sys::ImPlot_SetupAxisTicks_doublePtr(
445                    axis as sys::ImAxis,
446                    values.as_ptr(),
447                    count,
448                    ptrs.as_ptr() as *const *const c_char,
449                    keep_default,
450                )
451            })
452        } else {
453            unsafe {
454                sys::ImPlot_SetupAxisTicks_doublePtr(
455                    axis as sys::ImAxis,
456                    values.as_ptr(),
457                    count,
458                    std::ptr::null(),
459                    keep_default,
460                )
461            }
462        }
463    }
464
465    /// Setup ticks with explicit positions and optional labels for a Y axis.
466    ///
467    /// If `labels` is provided, it must have the same length as `values`.
468    pub fn setup_y_axis_ticks_positions(
469        &self,
470        axis: YAxis,
471        values: &[f64],
472        labels: Option<&[&str]>,
473        keep_default: bool,
474    ) {
475        let count = match i32::try_from(values.len()) {
476            Ok(v) => v,
477            Err(_) => return,
478        };
479        if let Some(labels) = labels {
480            if labels.len() != values.len() {
481                return;
482            }
483            let cleaned: Vec<&str> = labels
484                .iter()
485                .map(|&s| if s.contains('\0') { "" } else { s })
486                .collect();
487            with_scratch_txt_slice(&cleaned, |ptrs| unsafe {
488                sys::ImPlot_SetupAxisTicks_doublePtr(
489                    axis as sys::ImAxis,
490                    values.as_ptr(),
491                    count,
492                    ptrs.as_ptr() as *const *const c_char,
493                    keep_default,
494                )
495            })
496        } else {
497            unsafe {
498                sys::ImPlot_SetupAxisTicks_doublePtr(
499                    axis as sys::ImAxis,
500                    values.as_ptr(),
501                    count,
502                    std::ptr::null(),
503                    keep_default,
504                )
505            }
506        }
507    }
508
509    /// Setup ticks on a range with tick count and optional labels for an X axis.
510    ///
511    /// If `labels` is provided, it must have length `n_ticks`.
512    pub fn setup_x_axis_ticks_range(
513        &self,
514        axis: XAxis,
515        v_min: f64,
516        v_max: f64,
517        n_ticks: i32,
518        labels: Option<&[&str]>,
519        keep_default: bool,
520    ) {
521        if n_ticks <= 0 {
522            return;
523        }
524        if let Some(labels) = labels {
525            let Ok(ticks_usize) = usize::try_from(n_ticks) else {
526                return;
527            };
528            if labels.len() != ticks_usize {
529                return;
530            }
531            let cleaned: Vec<&str> = labels
532                .iter()
533                .map(|&s| if s.contains('\0') { "" } else { s })
534                .collect();
535            with_scratch_txt_slice(&cleaned, |ptrs| unsafe {
536                sys::ImPlot_SetupAxisTicks_double(
537                    axis as sys::ImAxis,
538                    v_min,
539                    v_max,
540                    n_ticks,
541                    ptrs.as_ptr() as *const *const c_char,
542                    keep_default,
543                )
544            })
545        } else {
546            unsafe {
547                sys::ImPlot_SetupAxisTicks_double(
548                    axis as sys::ImAxis,
549                    v_min,
550                    v_max,
551                    n_ticks,
552                    std::ptr::null(),
553                    keep_default,
554                )
555            }
556        }
557    }
558
559    /// Setup ticks on a range with tick count and optional labels for a Y axis.
560    ///
561    /// If `labels` is provided, it must have length `n_ticks`.
562    pub fn setup_y_axis_ticks_range(
563        &self,
564        axis: YAxis,
565        v_min: f64,
566        v_max: f64,
567        n_ticks: i32,
568        labels: Option<&[&str]>,
569        keep_default: bool,
570    ) {
571        if n_ticks <= 0 {
572            return;
573        }
574        if let Some(labels) = labels {
575            let Ok(ticks_usize) = usize::try_from(n_ticks) else {
576                return;
577            };
578            if labels.len() != ticks_usize {
579                return;
580            }
581            let cleaned: Vec<&str> = labels
582                .iter()
583                .map(|&s| if s.contains('\0') { "" } else { s })
584                .collect();
585            with_scratch_txt_slice(&cleaned, |ptrs| unsafe {
586                sys::ImPlot_SetupAxisTicks_double(
587                    axis as sys::ImAxis,
588                    v_min,
589                    v_max,
590                    n_ticks,
591                    ptrs.as_ptr() as *const *const c_char,
592                    keep_default,
593                )
594            })
595        } else {
596            unsafe {
597                sys::ImPlot_SetupAxisTicks_double(
598                    axis as sys::ImAxis,
599                    v_min,
600                    v_max,
601                    n_ticks,
602                    std::ptr::null(),
603                    keep_default,
604                )
605            }
606        }
607    }
608
609    /// Setup tick label format string for a specific X axis
610    pub fn setup_x_axis_format(&self, axis: XAxis, fmt: &str) {
611        if fmt.contains('\0') {
612            return;
613        }
614        with_scratch_txt(fmt, |ptr| unsafe {
615            sys::ImPlot_SetupAxisFormat_Str(axis as sys::ImAxis, ptr)
616        })
617    }
618
619    /// Setup tick label format string for a specific Y axis
620    pub fn setup_y_axis_format(&self, axis: YAxis, fmt: &str) {
621        if fmt.contains('\0') {
622            return;
623        }
624        with_scratch_txt(fmt, |ptr| unsafe {
625            sys::ImPlot_SetupAxisFormat_Str(axis as sys::ImAxis, ptr)
626        })
627    }
628
629    /// Setup scale for a specific X axis (pass sys::ImPlotScale variant)
630    pub fn setup_x_axis_scale(&self, axis: XAxis, scale: sys::ImPlotScale) {
631        unsafe { sys::ImPlot_SetupAxisScale_PlotScale(axis as sys::ImAxis, scale) }
632    }
633
634    /// Setup scale for a specific Y axis (pass sys::ImPlotScale variant)
635    pub fn setup_y_axis_scale(&self, axis: YAxis, scale: sys::ImPlotScale) {
636        unsafe { sys::ImPlot_SetupAxisScale_PlotScale(axis as sys::ImAxis, scale) }
637    }
638
639    /// Setup axis limits constraints
640    pub fn setup_axis_limits_constraints(&self, axis: i32, v_min: f64, v_max: f64) {
641        unsafe { sys::ImPlot_SetupAxisLimitsConstraints(axis as sys::ImAxis, v_min, v_max) }
642    }
643
644    /// Setup axis zoom constraints
645    pub fn setup_axis_zoom_constraints(&self, axis: i32, z_min: f64, z_max: f64) {
646        unsafe { sys::ImPlot_SetupAxisZoomConstraints(axis as sys::ImAxis, z_min, z_max) }
647    }
648
649    // -------- Formatter (closure) --------
650    /// Setup tick label formatter using a Rust closure.
651    ///
652    /// The closure is kept alive until the current plot ends.
653    pub fn setup_x_axis_format_closure<F>(&self, axis: XAxis, f: F) -> AxisFormatterToken
654    where
655        F: Fn(f64) -> String + Send + Sync + 'static,
656    {
657        AxisFormatterToken::new(axis as sys::ImAxis, f)
658    }
659
660    /// Setup tick label formatter using a Rust closure.
661    ///
662    /// The closure is kept alive until the current plot ends.
663    pub fn setup_y_axis_format_closure<F>(&self, axis: YAxis, f: F) -> AxisFormatterToken
664    where
665        F: Fn(f64) -> String + Send + Sync + 'static,
666    {
667        AxisFormatterToken::new(axis as sys::ImAxis, f)
668    }
669
670    // -------- Transform (closure) --------
671    /// Setup custom axis transform using Rust closures (forward/inverse).
672    ///
673    /// The closures are kept alive until the current plot ends.
674    pub fn setup_x_axis_transform_closure<FW, INV>(
675        &self,
676        axis: XAxis,
677        forward: FW,
678        inverse: INV,
679    ) -> AxisTransformToken
680    where
681        FW: Fn(f64) -> f64 + Send + Sync + 'static,
682        INV: Fn(f64) -> f64 + Send + Sync + 'static,
683    {
684        AxisTransformToken::new(axis as sys::ImAxis, forward, inverse)
685    }
686
687    /// Setup custom axis transform for Y axis using closures
688    pub fn setup_y_axis_transform_closure<FW, INV>(
689        &self,
690        axis: YAxis,
691        forward: FW,
692        inverse: INV,
693    ) -> AxisTransformToken
694    where
695        FW: Fn(f64) -> f64 + Send + Sync + 'static,
696        INV: Fn(f64) -> f64 + Send + Sync + 'static,
697    {
698        AxisTransformToken::new(axis as sys::ImAxis, forward, inverse)
699    }
700}
701
702// Plot-scope callback storage -------------------------------------------------
703//
704// ImPlot's axis formatter/transform APIs take function pointers + `user_data`
705// pointers, and may call them at any point until the current plot ends.
706//
707// Returning a standalone token that owns the closure is unsound: safe Rust code
708// could drop the token early, leaving ImPlot with a dangling `user_data` pointer.
709//
710// To keep the safe API sound without forcing users to manually retain tokens,
711// we store callback holders in thread-local, plot-scoped storage that is
712// created when a plot begins and destroyed when the plot ends.
713
714#[derive(Default)]
715struct PlotScopeStorage {
716    formatters: Vec<Box<FormatterHolder>>,
717    transforms: Vec<Box<TransformHolder>>,
718}
719
720thread_local! {
721    static PLOT_SCOPE_STACK: RefCell<Vec<PlotScopeStorage>> = const { RefCell::new(Vec::new()) };
722}
723
724fn with_plot_scope_storage<T>(f: impl FnOnce(&mut PlotScopeStorage) -> T) -> Option<T> {
725    PLOT_SCOPE_STACK.with(|stack| {
726        let mut stack = stack.borrow_mut();
727        stack.last_mut().map(f)
728    })
729}
730
731pub(crate) struct PlotScopeGuard {
732    _not_send_or_sync: std::marker::PhantomData<Rc<()>>,
733}
734
735impl PlotScopeGuard {
736    pub(crate) fn new() -> Self {
737        PLOT_SCOPE_STACK.with(|stack| stack.borrow_mut().push(PlotScopeStorage::default()));
738        Self {
739            _not_send_or_sync: std::marker::PhantomData,
740        }
741    }
742}
743
744impl Drop for PlotScopeGuard {
745    fn drop(&mut self) {
746        PLOT_SCOPE_STACK.with(|stack| {
747            let popped = stack.borrow_mut().pop();
748            debug_assert!(popped.is_some(), "dear-implot: plot scope stack underflow");
749        });
750    }
751}
752
753// =================== Formatter bridge ===================
754
755struct FormatterHolder {
756    func: Box<dyn Fn(f64) -> String + Send + Sync + 'static>,
757}
758
759#[must_use]
760pub struct AxisFormatterToken {
761    _private: (),
762}
763
764impl AxisFormatterToken {
765    fn new<F>(axis: sys::ImAxis, f: F) -> Self
766    where
767        F: Fn(f64) -> String + Send + Sync + 'static,
768    {
769        let configured = with_plot_scope_storage(|storage| {
770            let holder = Box::new(FormatterHolder { func: Box::new(f) });
771            let user = &*holder as *const FormatterHolder as *mut std::os::raw::c_void;
772            storage.formatters.push(holder);
773            unsafe {
774                sys::ImPlot_SetupAxisFormat_PlotFormatter(
775                    axis as sys::ImAxis,
776                    Some(formatter_thunk),
777                    user,
778                )
779            }
780        })
781        .is_some();
782
783        debug_assert!(
784            configured,
785            "dear-implot: axis formatter closure must be set within an active plot"
786        );
787
788        Self { _private: () }
789    }
790}
791
792impl Drop for AxisFormatterToken {
793    fn drop(&mut self) {
794        // The actual callback lifetime is managed by PlotScopeGuard.
795    }
796}
797
798unsafe extern "C" fn formatter_thunk(
799    value: f64,
800    buff: *mut std::os::raw::c_char,
801    size: std::os::raw::c_int,
802    user_data: *mut std::os::raw::c_void,
803) -> std::os::raw::c_int {
804    if user_data.is_null() || buff.is_null() || size <= 0 {
805        return 0;
806    }
807    // Safety: ImPlot passes back the same pointer we provided in `AxisFormatterToken::new`.
808    let holder = unsafe { &*(user_data as *const FormatterHolder) };
809    let s = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (holder.func)(value))) {
810        Ok(v) => v,
811        Err(_) => {
812            eprintln!("dear-implot: panic in axis formatter callback");
813            std::process::abort();
814        }
815    };
816    let bytes = s.as_bytes();
817    let max = (size - 1).max(0) as usize;
818    let n = bytes.len().min(max);
819
820    // Safety: `buff` is assumed to point to a valid buffer of at least `size`
821    // bytes, with space for a terminating null. This matches ImPlot's
822    // formatter contract.
823    unsafe {
824        std::ptr::copy_nonoverlapping(bytes.as_ptr(), buff as *mut u8, n);
825        *buff.add(n) = 0;
826    }
827    n as std::os::raw::c_int
828}
829
830// =================== Transform bridge ===================
831
832struct TransformHolder {
833    forward: Box<dyn Fn(f64) -> f64 + Send + Sync + 'static>,
834    inverse: Box<dyn Fn(f64) -> f64 + Send + Sync + 'static>,
835}
836
837#[must_use]
838pub struct AxisTransformToken {
839    _private: (),
840}
841
842impl AxisTransformToken {
843    fn new<FW, INV>(axis: sys::ImAxis, forward: FW, inverse: INV) -> Self
844    where
845        FW: Fn(f64) -> f64 + Send + Sync + 'static,
846        INV: Fn(f64) -> f64 + Send + Sync + 'static,
847    {
848        let configured = with_plot_scope_storage(|storage| {
849            let holder = Box::new(TransformHolder {
850                forward: Box::new(forward),
851                inverse: Box::new(inverse),
852            });
853            let user = &*holder as *const TransformHolder as *mut std::os::raw::c_void;
854            storage.transforms.push(holder);
855            unsafe {
856                sys::ImPlot_SetupAxisScale_PlotTransform(
857                    axis as sys::ImAxis,
858                    Some(transform_forward_thunk),
859                    Some(transform_inverse_thunk),
860                    user,
861                )
862            }
863        })
864        .is_some();
865
866        debug_assert!(
867            configured,
868            "dear-implot: axis transform closure must be set within an active plot"
869        );
870
871        Self { _private: () }
872    }
873}
874
875impl Drop for AxisTransformToken {
876    fn drop(&mut self) {
877        // The actual callback lifetime is managed by PlotScopeGuard.
878    }
879}
880
881unsafe extern "C" fn transform_forward_thunk(
882    value: f64,
883    user_data: *mut std::os::raw::c_void,
884) -> f64 {
885    if user_data.is_null() {
886        return value;
887    }
888    let holder = unsafe { &*(user_data as *const TransformHolder) };
889    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (holder.forward)(value))) {
890        Ok(v) => v,
891        Err(_) => {
892            eprintln!("dear-implot: panic in axis transform (forward) callback");
893            std::process::abort();
894        }
895    }
896}
897
898unsafe extern "C" fn transform_inverse_thunk(
899    value: f64,
900    user_data: *mut std::os::raw::c_void,
901) -> f64 {
902    if user_data.is_null() {
903        return value;
904    }
905    let holder = unsafe { &*(user_data as *const TransformHolder) };
906    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (holder.inverse)(value))) {
907        Ok(v) => v,
908        Err(_) => {
909            eprintln!("dear-implot: panic in axis transform (inverse) callback");
910            std::process::abort();
911        }
912    }
913}
914
915/// Token that represents an active plot
916///
917/// The plot will be automatically ended when this token is dropped.
918pub struct PlotToken<'ui> {
919    _scope: PlotScopeGuard,
920    _lifetime: std::marker::PhantomData<&'ui ()>,
921}
922
923impl<'ui> PlotToken<'ui> {
924    /// Create a new PlotToken (internal use only)
925    pub(crate) fn new() -> Self {
926        Self {
927            _scope: PlotScopeGuard::new(),
928            _lifetime: std::marker::PhantomData,
929        }
930    }
931
932    /// Manually end the plot
933    ///
934    /// This is called automatically when the token is dropped,
935    /// but you can call it manually if needed.
936    pub fn end(self) {
937        // The actual ending happens in Drop
938    }
939}
940
941impl<'ui> Drop for PlotToken<'ui> {
942    fn drop(&mut self) {
943        unsafe {
944            sys::ImPlot_EndPlot();
945        }
946    }
947}