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