Skip to main content

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