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    /// Plot a polygon with the given label and vertex data.
243    pub fn plot_polygon(&self, label: &str, x_data: &[f64], y_data: &[f64]) {
244        if x_data.len() != y_data.len() {
245            return;
246        }
247        let count = match i32::try_from(x_data.len()) {
248            Ok(v) => v,
249            Err(_) => return,
250        };
251
252        let label = if label.contains('\0') { "" } else { label };
253        with_scratch_txt(label, |ptr| unsafe {
254            let spec = crate::plots::plot_spec_from(0, 0, std::mem::size_of::<f64>() as i32);
255            sys::ImPlot_PlotPolygon_doublePtr(ptr, x_data.as_ptr(), y_data.as_ptr(), count, spec);
256        })
257    }
258
259    /// Check if the plot area is hovered
260    pub fn is_plot_hovered(&self) -> bool {
261        unsafe { sys::ImPlot_IsPlotHovered() }
262    }
263
264    /// Get the mouse position in plot coordinates
265    pub fn get_plot_mouse_pos(&self, y_axis: Option<crate::YAxisChoice>) -> sys::ImPlotPoint {
266        let y_axis_i32 = crate::y_axis_choice_option_to_i32(y_axis);
267        let y_axis = match y_axis_i32 {
268            0 => 3,
269            1 => 4,
270            2 => 5,
271            _ => 3,
272        };
273        unsafe { sys::ImPlot_GetPlotMousePos(0, y_axis) }
274    }
275
276    /// Get the mouse position in plot coordinates for specific axes
277    pub fn get_plot_mouse_pos_axes(&self, x_axis: XAxis, y_axis: YAxis) -> sys::ImPlotPoint {
278        unsafe { sys::ImPlot_GetPlotMousePos(x_axis as i32, y_axis as i32) }
279    }
280
281    /// Set current axes for subsequent plot submissions
282    pub fn set_axes(&self, x_axis: XAxis, y_axis: YAxis) {
283        unsafe { sys::ImPlot_SetAxes(x_axis as i32, y_axis as i32) }
284    }
285
286    /// Setup a specific X axis
287    pub fn setup_x_axis(&self, axis: XAxis, label: Option<&str>, flags: AxisFlags) {
288        let label = label.filter(|s| !s.contains('\0'));
289        match label {
290            Some(label) => with_scratch_txt(label, |ptr| unsafe {
291                sys::ImPlot_SetupAxis(
292                    axis as sys::ImAxis,
293                    ptr,
294                    flags.bits() as sys::ImPlotAxisFlags,
295                )
296            }),
297            None => unsafe {
298                sys::ImPlot_SetupAxis(
299                    axis as sys::ImAxis,
300                    std::ptr::null(),
301                    flags.bits() as sys::ImPlotAxisFlags,
302                )
303            },
304        }
305    }
306
307    /// Setup a specific Y axis
308    pub fn setup_y_axis(&self, axis: YAxis, label: Option<&str>, flags: AxisFlags) {
309        let label = label.filter(|s| !s.contains('\0'));
310        match label {
311            Some(label) => with_scratch_txt(label, |ptr| unsafe {
312                sys::ImPlot_SetupAxis(
313                    axis as sys::ImAxis,
314                    ptr,
315                    flags.bits() as sys::ImPlotAxisFlags,
316                )
317            }),
318            None => unsafe {
319                sys::ImPlot_SetupAxis(
320                    axis as sys::ImAxis,
321                    std::ptr::null(),
322                    flags.bits() as sys::ImPlotAxisFlags,
323                )
324            },
325        }
326    }
327
328    /// Setup axis limits for a specific X axis
329    pub fn setup_x_axis_limits(&self, axis: XAxis, min: f64, max: f64, cond: PlotCond) {
330        unsafe {
331            sys::ImPlot_SetupAxisLimits(axis as sys::ImAxis, min, max, cond as sys::ImPlotCond)
332        }
333    }
334
335    /// Setup axis limits for a specific Y axis
336    pub fn setup_y_axis_limits(&self, axis: YAxis, min: f64, max: f64, cond: PlotCond) {
337        unsafe {
338            sys::ImPlot_SetupAxisLimits(axis as sys::ImAxis, min, max, cond as sys::ImPlotCond)
339        }
340    }
341
342    /// Link an axis to external min/max values (live binding)
343    pub fn setup_axis_links(
344        &self,
345        axis: i32,
346        link_min: Option<&mut f64>,
347        link_max: Option<&mut f64>,
348    ) {
349        let pmin = link_min.map_or(std::ptr::null_mut(), |r| r as *mut f64);
350        let pmax = link_max.map_or(std::ptr::null_mut(), |r| r as *mut f64);
351        unsafe { sys::ImPlot_SetupAxisLinks(axis, pmin, pmax) }
352    }
353
354    /// Setup both axes labels/flags at once
355    pub fn setup_axes(
356        &self,
357        x_label: Option<&str>,
358        y_label: Option<&str>,
359        x_flags: AxisFlags,
360        y_flags: AxisFlags,
361    ) {
362        let x_label = x_label.filter(|s| !s.contains('\0'));
363        let y_label = y_label.filter(|s| !s.contains('\0'));
364
365        match (x_label, y_label) {
366            (Some(x_label), Some(y_label)) => {
367                with_scratch_txt_two(x_label, y_label, |xp, yp| unsafe {
368                    sys::ImPlot_SetupAxes(
369                        xp,
370                        yp,
371                        x_flags.bits() as sys::ImPlotAxisFlags,
372                        y_flags.bits() as sys::ImPlotAxisFlags,
373                    )
374                })
375            }
376            (Some(x_label), None) => with_scratch_txt(x_label, |xp| unsafe {
377                sys::ImPlot_SetupAxes(
378                    xp,
379                    std::ptr::null(),
380                    x_flags.bits() as sys::ImPlotAxisFlags,
381                    y_flags.bits() as sys::ImPlotAxisFlags,
382                )
383            }),
384            (None, Some(y_label)) => with_scratch_txt(y_label, |yp| unsafe {
385                sys::ImPlot_SetupAxes(
386                    std::ptr::null(),
387                    yp,
388                    x_flags.bits() as sys::ImPlotAxisFlags,
389                    y_flags.bits() as sys::ImPlotAxisFlags,
390                )
391            }),
392            (None, None) => unsafe {
393                sys::ImPlot_SetupAxes(
394                    std::ptr::null(),
395                    std::ptr::null(),
396                    x_flags.bits() as sys::ImPlotAxisFlags,
397                    y_flags.bits() as sys::ImPlotAxisFlags,
398                )
399            },
400        }
401    }
402
403    /// Setup axes limits (both) at once
404    pub fn setup_axes_limits(
405        &self,
406        x_min: f64,
407        x_max: f64,
408        y_min: f64,
409        y_max: f64,
410        cond: PlotCond,
411    ) {
412        unsafe { sys::ImPlot_SetupAxesLimits(x_min, x_max, y_min, y_max, cond as sys::ImPlotCond) }
413    }
414
415    /// Call after axis setup to finalize configuration
416    pub fn setup_finish(&self) {
417        unsafe { sys::ImPlot_SetupFinish() }
418    }
419
420    /// Set next frame limits for a specific axis
421    pub fn set_next_x_axis_limits(&self, axis: XAxis, min: f64, max: f64, cond: PlotCond) {
422        unsafe {
423            sys::ImPlot_SetNextAxisLimits(axis as sys::ImAxis, min, max, cond as sys::ImPlotCond)
424        }
425    }
426
427    /// Set next frame limits for a specific axis
428    pub fn set_next_y_axis_limits(&self, axis: YAxis, min: f64, max: f64, cond: PlotCond) {
429        unsafe {
430            sys::ImPlot_SetNextAxisLimits(axis as sys::ImAxis, min, max, cond as sys::ImPlotCond)
431        }
432    }
433
434    /// Link an axis to external min/max for next frame
435    pub fn set_next_axis_links(
436        &self,
437        axis: i32,
438        link_min: Option<&mut f64>,
439        link_max: Option<&mut f64>,
440    ) {
441        let pmin = link_min.map_or(std::ptr::null_mut(), |r| r as *mut f64);
442        let pmax = link_max.map_or(std::ptr::null_mut(), |r| r as *mut f64);
443        unsafe { sys::ImPlot_SetNextAxisLinks(axis, pmin, pmax) }
444    }
445
446    /// Set next frame limits for both axes
447    pub fn set_next_axes_limits(
448        &self,
449        x_min: f64,
450        x_max: f64,
451        y_min: f64,
452        y_max: f64,
453        cond: PlotCond,
454    ) {
455        unsafe {
456            sys::ImPlot_SetNextAxesLimits(x_min, x_max, y_min, y_max, cond as sys::ImPlotCond)
457        }
458    }
459
460    /// Fit next frame both axes
461    pub fn set_next_axes_to_fit(&self) {
462        unsafe { sys::ImPlot_SetNextAxesToFit() }
463    }
464
465    /// Fit next frame a specific axis (raw)
466    pub fn set_next_axis_to_fit(&self, axis: i32) {
467        unsafe { sys::ImPlot_SetNextAxisToFit(axis as sys::ImAxis) }
468    }
469
470    /// Fit next frame a specific X axis
471    pub fn set_next_x_axis_to_fit(&self, axis: XAxis) {
472        unsafe { sys::ImPlot_SetNextAxisToFit(axis as sys::ImAxis) }
473    }
474
475    /// Fit next frame a specific Y axis
476    pub fn set_next_y_axis_to_fit(&self, axis: YAxis) {
477        unsafe { sys::ImPlot_SetNextAxisToFit(axis as sys::ImAxis) }
478    }
479
480    /// Setup ticks with explicit positions and optional labels for an X axis.
481    ///
482    /// If `labels` is provided, it must have the same length as `values`.
483    pub fn setup_x_axis_ticks_positions(
484        &self,
485        axis: XAxis,
486        values: &[f64],
487        labels: Option<&[&str]>,
488        keep_default: bool,
489    ) {
490        let count = match i32::try_from(values.len()) {
491            Ok(v) => v,
492            Err(_) => return,
493        };
494        if let Some(labels) = labels {
495            if labels.len() != values.len() {
496                return;
497            }
498            let cleaned: Vec<&str> = labels
499                .iter()
500                .map(|&s| if s.contains('\0') { "" } else { s })
501                .collect();
502            with_scratch_txt_slice(&cleaned, |ptrs| unsafe {
503                sys::ImPlot_SetupAxisTicks_doublePtr(
504                    axis as sys::ImAxis,
505                    values.as_ptr(),
506                    count,
507                    ptrs.as_ptr() as *const *const c_char,
508                    keep_default,
509                )
510            })
511        } else {
512            unsafe {
513                sys::ImPlot_SetupAxisTicks_doublePtr(
514                    axis as sys::ImAxis,
515                    values.as_ptr(),
516                    count,
517                    std::ptr::null(),
518                    keep_default,
519                )
520            }
521        }
522    }
523
524    /// Setup ticks with explicit positions and optional labels for a Y axis.
525    ///
526    /// If `labels` is provided, it must have the same length as `values`.
527    pub fn setup_y_axis_ticks_positions(
528        &self,
529        axis: YAxis,
530        values: &[f64],
531        labels: Option<&[&str]>,
532        keep_default: bool,
533    ) {
534        let count = match i32::try_from(values.len()) {
535            Ok(v) => v,
536            Err(_) => return,
537        };
538        if let Some(labels) = labels {
539            if labels.len() != values.len() {
540                return;
541            }
542            let cleaned: Vec<&str> = labels
543                .iter()
544                .map(|&s| if s.contains('\0') { "" } else { s })
545                .collect();
546            with_scratch_txt_slice(&cleaned, |ptrs| unsafe {
547                sys::ImPlot_SetupAxisTicks_doublePtr(
548                    axis as sys::ImAxis,
549                    values.as_ptr(),
550                    count,
551                    ptrs.as_ptr() as *const *const c_char,
552                    keep_default,
553                )
554            })
555        } else {
556            unsafe {
557                sys::ImPlot_SetupAxisTicks_doublePtr(
558                    axis as sys::ImAxis,
559                    values.as_ptr(),
560                    count,
561                    std::ptr::null(),
562                    keep_default,
563                )
564            }
565        }
566    }
567
568    /// Setup ticks on a range with tick count and optional labels for an X axis.
569    ///
570    /// If `labels` is provided, it must have length `n_ticks`.
571    pub fn setup_x_axis_ticks_range(
572        &self,
573        axis: XAxis,
574        v_min: f64,
575        v_max: f64,
576        n_ticks: i32,
577        labels: Option<&[&str]>,
578        keep_default: bool,
579    ) {
580        if n_ticks <= 0 {
581            return;
582        }
583        if let Some(labels) = labels {
584            let Ok(ticks_usize) = usize::try_from(n_ticks) else {
585                return;
586            };
587            if labels.len() != ticks_usize {
588                return;
589            }
590            let cleaned: Vec<&str> = labels
591                .iter()
592                .map(|&s| if s.contains('\0') { "" } else { s })
593                .collect();
594            with_scratch_txt_slice(&cleaned, |ptrs| unsafe {
595                sys::ImPlot_SetupAxisTicks_double(
596                    axis as sys::ImAxis,
597                    v_min,
598                    v_max,
599                    n_ticks,
600                    ptrs.as_ptr() as *const *const c_char,
601                    keep_default,
602                )
603            })
604        } else {
605            unsafe {
606                sys::ImPlot_SetupAxisTicks_double(
607                    axis as sys::ImAxis,
608                    v_min,
609                    v_max,
610                    n_ticks,
611                    std::ptr::null(),
612                    keep_default,
613                )
614            }
615        }
616    }
617
618    /// Setup ticks on a range with tick count and optional labels for a Y axis.
619    ///
620    /// If `labels` is provided, it must have length `n_ticks`.
621    pub fn setup_y_axis_ticks_range(
622        &self,
623        axis: YAxis,
624        v_min: f64,
625        v_max: f64,
626        n_ticks: i32,
627        labels: Option<&[&str]>,
628        keep_default: bool,
629    ) {
630        if n_ticks <= 0 {
631            return;
632        }
633        if let Some(labels) = labels {
634            let Ok(ticks_usize) = usize::try_from(n_ticks) else {
635                return;
636            };
637            if labels.len() != ticks_usize {
638                return;
639            }
640            let cleaned: Vec<&str> = labels
641                .iter()
642                .map(|&s| if s.contains('\0') { "" } else { s })
643                .collect();
644            with_scratch_txt_slice(&cleaned, |ptrs| unsafe {
645                sys::ImPlot_SetupAxisTicks_double(
646                    axis as sys::ImAxis,
647                    v_min,
648                    v_max,
649                    n_ticks,
650                    ptrs.as_ptr() as *const *const c_char,
651                    keep_default,
652                )
653            })
654        } else {
655            unsafe {
656                sys::ImPlot_SetupAxisTicks_double(
657                    axis as sys::ImAxis,
658                    v_min,
659                    v_max,
660                    n_ticks,
661                    std::ptr::null(),
662                    keep_default,
663                )
664            }
665        }
666    }
667
668    /// Setup tick label format string for a specific X axis
669    pub fn setup_x_axis_format(&self, axis: XAxis, fmt: &str) {
670        if fmt.contains('\0') {
671            return;
672        }
673        with_scratch_txt(fmt, |ptr| unsafe {
674            sys::ImPlot_SetupAxisFormat_Str(axis as sys::ImAxis, ptr)
675        })
676    }
677
678    /// Setup tick label format string for a specific Y axis
679    pub fn setup_y_axis_format(&self, axis: YAxis, fmt: &str) {
680        if fmt.contains('\0') {
681            return;
682        }
683        with_scratch_txt(fmt, |ptr| unsafe {
684            sys::ImPlot_SetupAxisFormat_Str(axis as sys::ImAxis, ptr)
685        })
686    }
687
688    /// Setup scale for a specific X axis (pass sys::ImPlotScale variant)
689    pub fn setup_x_axis_scale(&self, axis: XAxis, scale: sys::ImPlotScale) {
690        unsafe { sys::ImPlot_SetupAxisScale_PlotScale(axis as sys::ImAxis, scale) }
691    }
692
693    /// Setup scale for a specific Y axis (pass sys::ImPlotScale variant)
694    pub fn setup_y_axis_scale(&self, axis: YAxis, scale: sys::ImPlotScale) {
695        unsafe { sys::ImPlot_SetupAxisScale_PlotScale(axis as sys::ImAxis, scale) }
696    }
697
698    /// Setup axis limits constraints
699    pub fn setup_axis_limits_constraints(&self, axis: i32, v_min: f64, v_max: f64) {
700        unsafe { sys::ImPlot_SetupAxisLimitsConstraints(axis as sys::ImAxis, v_min, v_max) }
701    }
702
703    /// Setup axis zoom constraints
704    pub fn setup_axis_zoom_constraints(&self, axis: i32, z_min: f64, z_max: f64) {
705        unsafe { sys::ImPlot_SetupAxisZoomConstraints(axis as sys::ImAxis, z_min, z_max) }
706    }
707
708    // -------- Formatter (closure) --------
709    /// Setup tick label formatter using a Rust closure.
710    ///
711    /// The closure is kept alive until the current plot ends.
712    pub fn setup_x_axis_format_closure<F>(&self, axis: XAxis, f: F) -> AxisFormatterToken
713    where
714        F: Fn(f64) -> String + Send + Sync + 'static,
715    {
716        AxisFormatterToken::new(axis as sys::ImAxis, f)
717    }
718
719    /// Setup tick label formatter using a Rust closure.
720    ///
721    /// The closure is kept alive until the current plot ends.
722    pub fn setup_y_axis_format_closure<F>(&self, axis: YAxis, f: F) -> AxisFormatterToken
723    where
724        F: Fn(f64) -> String + Send + Sync + 'static,
725    {
726        AxisFormatterToken::new(axis as sys::ImAxis, f)
727    }
728
729    // -------- Transform (closure) --------
730    /// Setup custom axis transform using Rust closures (forward/inverse).
731    ///
732    /// The closures are kept alive until the current plot ends.
733    pub fn setup_x_axis_transform_closure<FW, INV>(
734        &self,
735        axis: XAxis,
736        forward: FW,
737        inverse: INV,
738    ) -> AxisTransformToken
739    where
740        FW: Fn(f64) -> f64 + Send + Sync + 'static,
741        INV: Fn(f64) -> f64 + Send + Sync + 'static,
742    {
743        AxisTransformToken::new(axis as sys::ImAxis, forward, inverse)
744    }
745
746    /// Setup custom axis transform for Y axis using closures
747    pub fn setup_y_axis_transform_closure<FW, INV>(
748        &self,
749        axis: YAxis,
750        forward: FW,
751        inverse: INV,
752    ) -> AxisTransformToken
753    where
754        FW: Fn(f64) -> f64 + Send + Sync + 'static,
755        INV: Fn(f64) -> f64 + Send + Sync + 'static,
756    {
757        AxisTransformToken::new(axis as sys::ImAxis, forward, inverse)
758    }
759}
760
761// Plot-scope callback storage -------------------------------------------------
762//
763// ImPlot's axis formatter/transform APIs take function pointers + `user_data`
764// pointers, and may call them at any point until the current plot ends.
765//
766// Returning a standalone token that owns the closure is unsound: safe Rust code
767// could drop the token early, leaving ImPlot with a dangling `user_data` pointer.
768//
769// To keep the safe API sound without forcing users to manually retain tokens,
770// we store callback holders in thread-local, plot-scoped storage that is
771// created when a plot begins and destroyed when the plot ends.
772
773#[derive(Default)]
774struct PlotScopeStorage {
775    formatters: Vec<Box<FormatterHolder>>,
776    transforms: Vec<Box<TransformHolder>>,
777}
778
779thread_local! {
780    static PLOT_SCOPE_STACK: RefCell<Vec<PlotScopeStorage>> = const { RefCell::new(Vec::new()) };
781}
782
783fn with_plot_scope_storage<T>(f: impl FnOnce(&mut PlotScopeStorage) -> T) -> Option<T> {
784    PLOT_SCOPE_STACK.with(|stack| {
785        let mut stack = stack.borrow_mut();
786        stack.last_mut().map(f)
787    })
788}
789
790pub(crate) struct PlotScopeGuard {
791    _not_send_or_sync: std::marker::PhantomData<Rc<()>>,
792}
793
794impl PlotScopeGuard {
795    pub(crate) fn new() -> Self {
796        PLOT_SCOPE_STACK.with(|stack| stack.borrow_mut().push(PlotScopeStorage::default()));
797        Self {
798            _not_send_or_sync: std::marker::PhantomData,
799        }
800    }
801}
802
803impl Drop for PlotScopeGuard {
804    fn drop(&mut self) {
805        PLOT_SCOPE_STACK.with(|stack| {
806            let popped = stack.borrow_mut().pop();
807            debug_assert!(popped.is_some(), "dear-implot: plot scope stack underflow");
808        });
809    }
810}
811
812// =================== Formatter bridge ===================
813
814struct FormatterHolder {
815    func: Box<dyn Fn(f64) -> String + Send + Sync + 'static>,
816}
817
818#[must_use]
819pub struct AxisFormatterToken {
820    _private: (),
821}
822
823impl AxisFormatterToken {
824    fn new<F>(axis: sys::ImAxis, f: F) -> Self
825    where
826        F: Fn(f64) -> String + Send + Sync + 'static,
827    {
828        let configured = with_plot_scope_storage(|storage| {
829            let holder = Box::new(FormatterHolder { func: Box::new(f) });
830            let user = &*holder as *const FormatterHolder as *mut std::os::raw::c_void;
831            storage.formatters.push(holder);
832            unsafe {
833                sys::ImPlot_SetupAxisFormat_PlotFormatter(
834                    axis as sys::ImAxis,
835                    Some(formatter_thunk),
836                    user,
837                )
838            }
839        })
840        .is_some();
841
842        debug_assert!(
843            configured,
844            "dear-implot: axis formatter closure must be set within an active plot"
845        );
846
847        Self { _private: () }
848    }
849}
850
851impl Drop for AxisFormatterToken {
852    fn drop(&mut self) {
853        // The actual callback lifetime is managed by PlotScopeGuard.
854    }
855}
856
857unsafe extern "C" fn formatter_thunk(
858    value: f64,
859    buff: *mut std::os::raw::c_char,
860    size: std::os::raw::c_int,
861    user_data: *mut std::os::raw::c_void,
862) -> std::os::raw::c_int {
863    if user_data.is_null() || buff.is_null() || size <= 0 {
864        return 0;
865    }
866    // Safety: ImPlot passes back the same pointer we provided in `AxisFormatterToken::new`.
867    let holder = unsafe { &*(user_data as *const FormatterHolder) };
868    let s = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (holder.func)(value))) {
869        Ok(v) => v,
870        Err(_) => {
871            eprintln!("dear-implot: panic in axis formatter callback");
872            std::process::abort();
873        }
874    };
875    let bytes = s.as_bytes();
876    let max = (size - 1).max(0) as usize;
877    let n = bytes.len().min(max);
878
879    // Safety: `buff` is assumed to point to a valid buffer of at least `size`
880    // bytes, with space for a terminating null. This matches ImPlot's
881    // formatter contract.
882    unsafe {
883        std::ptr::copy_nonoverlapping(bytes.as_ptr(), buff as *mut u8, n);
884        *buff.add(n) = 0;
885    }
886    n as std::os::raw::c_int
887}
888
889// =================== Transform bridge ===================
890
891struct TransformHolder {
892    forward: Box<dyn Fn(f64) -> f64 + Send + Sync + 'static>,
893    inverse: Box<dyn Fn(f64) -> f64 + Send + Sync + 'static>,
894}
895
896#[must_use]
897pub struct AxisTransformToken {
898    _private: (),
899}
900
901impl AxisTransformToken {
902    fn new<FW, INV>(axis: sys::ImAxis, forward: FW, inverse: INV) -> Self
903    where
904        FW: Fn(f64) -> f64 + Send + Sync + 'static,
905        INV: Fn(f64) -> f64 + Send + Sync + 'static,
906    {
907        let configured = with_plot_scope_storage(|storage| {
908            let holder = Box::new(TransformHolder {
909                forward: Box::new(forward),
910                inverse: Box::new(inverse),
911            });
912            let user = &*holder as *const TransformHolder as *mut std::os::raw::c_void;
913            storage.transforms.push(holder);
914            unsafe {
915                sys::ImPlot_SetupAxisScale_PlotTransform(
916                    axis as sys::ImAxis,
917                    Some(transform_forward_thunk),
918                    Some(transform_inverse_thunk),
919                    user,
920                )
921            }
922        })
923        .is_some();
924
925        debug_assert!(
926            configured,
927            "dear-implot: axis transform closure must be set within an active plot"
928        );
929
930        Self { _private: () }
931    }
932}
933
934impl Drop for AxisTransformToken {
935    fn drop(&mut self) {
936        // The actual callback lifetime is managed by PlotScopeGuard.
937    }
938}
939
940unsafe extern "C" fn transform_forward_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.forward)(value))) {
949        Ok(v) => v,
950        Err(_) => {
951            eprintln!("dear-implot: panic in axis transform (forward) callback");
952            std::process::abort();
953        }
954    }
955}
956
957unsafe extern "C" fn transform_inverse_thunk(
958    value: f64,
959    user_data: *mut std::os::raw::c_void,
960) -> f64 {
961    if user_data.is_null() {
962        return value;
963    }
964    let holder = unsafe { &*(user_data as *const TransformHolder) };
965    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (holder.inverse)(value))) {
966        Ok(v) => v,
967        Err(_) => {
968            eprintln!("dear-implot: panic in axis transform (inverse) callback");
969            std::process::abort();
970        }
971    }
972}
973
974/// Token that represents an active plot
975///
976/// The plot will be automatically ended when this token is dropped.
977pub struct PlotToken<'ui> {
978    _scope: PlotScopeGuard,
979    _lifetime: std::marker::PhantomData<&'ui ()>,
980}
981
982impl<'ui> PlotToken<'ui> {
983    /// Create a new PlotToken (internal use only)
984    pub(crate) fn new() -> Self {
985        Self {
986            _scope: PlotScopeGuard::new(),
987            _lifetime: std::marker::PhantomData,
988        }
989    }
990
991    /// Manually end the plot
992    ///
993    /// This is called automatically when the token is dropped,
994    /// but you can call it manually if needed.
995    pub fn end(self) {
996        // The actual ending happens in Drop
997    }
998}
999
1000impl<'ui> Drop for PlotToken<'ui> {
1001    fn drop(&mut self) {
1002        unsafe {
1003            sys::ImPlot_EndPlot();
1004        }
1005    }
1006}