dear_implot/
context.rs

1use crate::{AxisFlags, PlotCond, XAxis, YAxis, sys};
2use dear_imgui_rs::{Context as ImGuiContext, Ui};
3use dear_imgui_sys as imgui_sys;
4
5/// ImPlot context that manages the plotting state
6///
7/// This context is separate from the Dear ImGui context but works alongside it.
8/// You need both contexts to create plots.
9pub struct PlotContext {
10    raw: *mut sys::ImPlotContext,
11}
12
13impl PlotContext {
14    /// Try to create a new ImPlot context
15    ///
16    /// This should be called after creating the Dear ImGui context.
17    /// The ImPlot context will use the same Dear ImGui context internally.
18    pub fn try_create(_imgui_ctx: &ImGuiContext) -> dear_imgui_rs::ImGuiResult<Self> {
19        // Bind ImPlot to the current Dear ImGui context before creating.
20        // On some toolchains/platforms, not setting this can lead to crashes
21        // if ImPlot initialization queries ImGui state during CreateContext.
22        unsafe {
23            sys::ImPlot_SetImGuiContext(imgui_sys::igGetCurrentContext());
24        }
25
26        let raw = unsafe { sys::ImPlot_CreateContext() };
27        if raw.is_null() {
28            return Err(dear_imgui_rs::ImGuiError::context_creation(
29                "ImPlot_CreateContext returned null",
30            ));
31        }
32
33        // Ensure the newly created context is current (defensive, CreateContext should do this).
34        unsafe {
35            sys::ImPlot_SetCurrentContext(raw);
36        }
37
38        Ok(Self { raw })
39    }
40
41    /// Create a new ImPlot context (panics on error)
42    pub fn create(imgui_ctx: &ImGuiContext) -> Self {
43        Self::try_create(imgui_ctx).expect("Failed to create ImPlot context")
44    }
45
46    /// Get the current ImPlot context
47    ///
48    /// Returns None if no context is current
49    pub fn current() -> Option<Self> {
50        let raw = unsafe { sys::ImPlot_GetCurrentContext() };
51        if raw.is_null() {
52            None
53        } else {
54            Some(Self { raw })
55        }
56    }
57
58    /// Set this context as the current ImPlot context
59    pub fn set_as_current(&self) {
60        unsafe {
61            sys::ImPlot_SetCurrentContext(self.raw);
62        }
63    }
64
65    /// Get a PlotUi for creating plots
66    ///
67    /// This borrows both the ImPlot context and the Dear ImGui Ui,
68    /// ensuring that plots can only be created when both are available.
69    pub fn get_plot_ui<'ui>(&'ui self, ui: &'ui Ui) -> PlotUi<'ui> {
70        PlotUi { context: self, ui }
71    }
72
73    /// Get the raw ImPlot context pointer
74    ///
75    /// # Safety
76    ///
77    /// The caller must ensure the pointer is used safely and not stored
78    /// beyond the lifetime of this context.
79    pub unsafe fn raw(&self) -> *mut sys::ImPlotContext {
80        self.raw
81    }
82}
83
84impl Drop for PlotContext {
85    fn drop(&mut self) {
86        if !self.raw.is_null() {
87            unsafe {
88                sys::ImPlot_DestroyContext(self.raw);
89            }
90        }
91    }
92}
93
94// ImPlot context is tied to Dear ImGui and not thread-safe to send/share.
95
96/// A temporary reference for building plots
97///
98/// This struct ensures that plots can only be created when both ImGui and ImPlot
99/// contexts are available and properly set up.
100pub struct PlotUi<'ui> {
101    #[allow(dead_code)]
102    context: &'ui PlotContext,
103    #[allow(dead_code)]
104    ui: &'ui Ui,
105}
106
107impl<'ui> PlotUi<'ui> {
108    /// Begin a new plot with the given title
109    ///
110    /// Returns a PlotToken if the plot was successfully started.
111    /// The plot will be automatically ended when the token is dropped.
112    pub fn begin_plot(&self, title: &str) -> Option<PlotToken<'_>> {
113        let title_cstr = std::ffi::CString::new(title).ok()?;
114
115        let size = sys::ImVec2_c { x: -1.0, y: 0.0 };
116        let started = unsafe { sys::ImPlot_BeginPlot(title_cstr.as_ptr(), size, 0) };
117
118        if started {
119            Some(PlotToken::new())
120        } else {
121            None
122        }
123    }
124
125    /// Begin a plot with custom size
126    pub fn begin_plot_with_size(&self, title: &str, size: [f32; 2]) -> Option<PlotToken<'_>> {
127        let title_cstr = std::ffi::CString::new(title).ok()?;
128
129        let plot_size = sys::ImVec2_c {
130            x: size[0],
131            y: size[1],
132        };
133        let started = unsafe { sys::ImPlot_BeginPlot(title_cstr.as_ptr(), plot_size, 0) };
134
135        if started {
136            Some(PlotToken::new())
137        } else {
138            None
139        }
140    }
141
142    /// Plot a line with the given label and data
143    ///
144    /// This is a convenience method that can be called within a plot.
145    pub fn plot_line(&self, label: &str, x_data: &[f64], y_data: &[f64]) {
146        if x_data.len() != y_data.len() {
147            return; // Data length mismatch
148        }
149
150        let label_cstr = std::ffi::CString::new(label).unwrap_or_default();
151
152        unsafe {
153            sys::ImPlot_PlotLine_doublePtrdoublePtr(
154                label_cstr.as_ptr(),
155                x_data.as_ptr(),
156                y_data.as_ptr(),
157                x_data.len() as i32,
158                0,
159                0,
160                0,
161            );
162        }
163    }
164
165    /// Plot a scatter plot with the given label and data
166    pub fn plot_scatter(&self, label: &str, x_data: &[f64], y_data: &[f64]) {
167        if x_data.len() != y_data.len() {
168            return; // Data length mismatch
169        }
170
171        let label_cstr = std::ffi::CString::new(label).unwrap_or_default();
172
173        unsafe {
174            sys::ImPlot_PlotScatter_doublePtrdoublePtr(
175                label_cstr.as_ptr(),
176                x_data.as_ptr(),
177                y_data.as_ptr(),
178                x_data.len() as i32,
179                0,
180                0,
181                0,
182            );
183        }
184    }
185
186    /// Check if the plot area is hovered
187    pub fn is_plot_hovered(&self) -> bool {
188        unsafe { sys::ImPlot_IsPlotHovered() }
189    }
190
191    /// Get the mouse position in plot coordinates
192    pub fn get_plot_mouse_pos(&self, y_axis: Option<crate::YAxisChoice>) -> sys::ImPlotPoint {
193        let y_axis_i32 = crate::y_axis_choice_option_to_i32(y_axis);
194        let y_axis = match y_axis_i32 {
195            0 => 3,
196            1 => 4,
197            2 => 5,
198            _ => 3,
199        };
200        unsafe { sys::ImPlot_GetPlotMousePos(0, y_axis) }
201    }
202
203    /// Get the mouse position in plot coordinates for specific axes
204    pub fn get_plot_mouse_pos_axes(&self, x_axis: XAxis, y_axis: YAxis) -> sys::ImPlotPoint {
205        unsafe { sys::ImPlot_GetPlotMousePos(x_axis as i32, y_axis as i32) }
206    }
207
208    /// Set current axes for subsequent plot submissions
209    pub fn set_axes(&self, x_axis: XAxis, y_axis: YAxis) {
210        unsafe { sys::ImPlot_SetAxes(x_axis as i32, y_axis as i32) }
211    }
212
213    /// Setup a specific X axis
214    pub fn setup_x_axis(&self, axis: XAxis, label: Option<&str>, flags: AxisFlags) {
215        let label_cstr = label.and_then(|s| std::ffi::CString::new(s).ok());
216        let ptr = label_cstr
217            .as_ref()
218            .map(|c| c.as_ptr())
219            .unwrap_or(std::ptr::null());
220        unsafe {
221            sys::ImPlot_SetupAxis(
222                axis as sys::ImAxis,
223                ptr,
224                flags.bits() as sys::ImPlotAxisFlags,
225            )
226        }
227    }
228
229    /// Setup a specific Y axis
230    pub fn setup_y_axis(&self, axis: YAxis, label: Option<&str>, flags: AxisFlags) {
231        let label_cstr = label.and_then(|s| std::ffi::CString::new(s).ok());
232        let ptr = label_cstr
233            .as_ref()
234            .map(|c| c.as_ptr())
235            .unwrap_or(std::ptr::null());
236        unsafe {
237            sys::ImPlot_SetupAxis(
238                axis as sys::ImAxis,
239                ptr,
240                flags.bits() as sys::ImPlotAxisFlags,
241            )
242        }
243    }
244
245    /// Setup axis limits for a specific X axis
246    pub fn setup_x_axis_limits(&self, axis: XAxis, min: f64, max: f64, cond: PlotCond) {
247        unsafe {
248            sys::ImPlot_SetupAxisLimits(axis as sys::ImAxis, min, max, cond as sys::ImPlotCond)
249        }
250    }
251
252    /// Setup axis limits for a specific Y axis
253    pub fn setup_y_axis_limits(&self, axis: YAxis, min: f64, max: f64, cond: PlotCond) {
254        unsafe {
255            sys::ImPlot_SetupAxisLimits(axis as sys::ImAxis, min, max, cond as sys::ImPlotCond)
256        }
257    }
258
259    /// Link an axis to external min/max values (live binding)
260    pub fn setup_axis_links(
261        &self,
262        axis: i32,
263        link_min: Option<&mut f64>,
264        link_max: Option<&mut f64>,
265    ) {
266        let pmin = link_min.map_or(std::ptr::null_mut(), |r| r as *mut f64);
267        let pmax = link_max.map_or(std::ptr::null_mut(), |r| r as *mut f64);
268        unsafe { sys::ImPlot_SetupAxisLinks(axis, pmin, pmax) }
269    }
270
271    /// Setup both axes labels/flags at once
272    pub fn setup_axes(
273        &self,
274        x_label: Option<&str>,
275        y_label: Option<&str>,
276        x_flags: AxisFlags,
277        y_flags: AxisFlags,
278    ) {
279        let x_c = x_label.and_then(|s| std::ffi::CString::new(s).ok());
280        let y_c = y_label.and_then(|s| std::ffi::CString::new(s).ok());
281        let xp = x_c.as_ref().map(|c| c.as_ptr()).unwrap_or(std::ptr::null());
282        let yp = y_c.as_ref().map(|c| c.as_ptr()).unwrap_or(std::ptr::null());
283        unsafe {
284            sys::ImPlot_SetupAxes(
285                xp,
286                yp,
287                x_flags.bits() as sys::ImPlotAxisFlags,
288                y_flags.bits() as sys::ImPlotAxisFlags,
289            )
290        }
291    }
292
293    /// Setup axes limits (both) at once
294    pub fn setup_axes_limits(
295        &self,
296        x_min: f64,
297        x_max: f64,
298        y_min: f64,
299        y_max: f64,
300        cond: PlotCond,
301    ) {
302        unsafe { sys::ImPlot_SetupAxesLimits(x_min, x_max, y_min, y_max, cond as sys::ImPlotCond) }
303    }
304
305    /// Call after axis setup to finalize configuration
306    pub fn setup_finish(&self) {
307        unsafe { sys::ImPlot_SetupFinish() }
308    }
309
310    /// Set next frame limits for a specific axis
311    pub fn set_next_x_axis_limits(&self, axis: XAxis, min: f64, max: f64, cond: PlotCond) {
312        unsafe {
313            sys::ImPlot_SetNextAxisLimits(axis as sys::ImAxis, min, max, cond as sys::ImPlotCond)
314        }
315    }
316
317    /// Set next frame limits for a specific axis
318    pub fn set_next_y_axis_limits(&self, axis: YAxis, min: f64, max: f64, cond: PlotCond) {
319        unsafe {
320            sys::ImPlot_SetNextAxisLimits(axis as sys::ImAxis, min, max, cond as sys::ImPlotCond)
321        }
322    }
323
324    /// Link an axis to external min/max for next frame
325    pub fn set_next_axis_links(
326        &self,
327        axis: i32,
328        link_min: Option<&mut f64>,
329        link_max: Option<&mut f64>,
330    ) {
331        let pmin = link_min.map_or(std::ptr::null_mut(), |r| r as *mut f64);
332        let pmax = link_max.map_or(std::ptr::null_mut(), |r| r as *mut f64);
333        unsafe { sys::ImPlot_SetNextAxisLinks(axis, pmin, pmax) }
334    }
335
336    /// Set next frame limits for both axes
337    pub fn set_next_axes_limits(
338        &self,
339        x_min: f64,
340        x_max: f64,
341        y_min: f64,
342        y_max: f64,
343        cond: PlotCond,
344    ) {
345        unsafe {
346            sys::ImPlot_SetNextAxesLimits(x_min, x_max, y_min, y_max, cond as sys::ImPlotCond)
347        }
348    }
349
350    /// Fit next frame both axes
351    pub fn set_next_axes_to_fit(&self) {
352        unsafe { sys::ImPlot_SetNextAxesToFit() }
353    }
354
355    /// Fit next frame a specific axis (raw)
356    pub fn set_next_axis_to_fit(&self, axis: i32) {
357        unsafe { sys::ImPlot_SetNextAxisToFit(axis as sys::ImAxis) }
358    }
359
360    /// Fit next frame a specific X axis
361    pub fn set_next_x_axis_to_fit(&self, axis: XAxis) {
362        unsafe { sys::ImPlot_SetNextAxisToFit(axis as sys::ImAxis) }
363    }
364
365    /// Fit next frame a specific Y axis
366    pub fn set_next_y_axis_to_fit(&self, axis: YAxis) {
367        unsafe { sys::ImPlot_SetNextAxisToFit(axis as sys::ImAxis) }
368    }
369
370    /// Setup ticks with explicit positions and optional labels for an X axis
371    pub fn setup_x_axis_ticks_positions(
372        &self,
373        axis: XAxis,
374        values: &[f64],
375        labels: Option<&[&str]>,
376        keep_default: bool,
377    ) {
378        let cstrs: Option<Vec<std::ffi::CString>> = labels.map(|ls| {
379            ls.iter()
380                .map(|&s| std::ffi::CString::new(s).unwrap_or_default())
381                .collect()
382        });
383        // To keep lifetimes, allocate a temp Vec if labels present
384        if let Some(vec) = cstrs {
385            let raw: Vec<*const i8> = vec.iter().map(|c| c.as_ptr()).collect();
386            unsafe {
387                sys::ImPlot_SetupAxisTicks_doublePtr(
388                    axis as sys::ImAxis,
389                    values.as_ptr(),
390                    values.len() as i32,
391                    raw.as_ptr(),
392                    keep_default,
393                )
394            }
395        } else {
396            unsafe {
397                sys::ImPlot_SetupAxisTicks_doublePtr(
398                    axis as sys::ImAxis,
399                    values.as_ptr(),
400                    values.len() as i32,
401                    std::ptr::null(),
402                    keep_default,
403                )
404            }
405        }
406    }
407
408    /// Setup ticks with explicit positions and optional labels for a Y axis
409    pub fn setup_y_axis_ticks_positions(
410        &self,
411        axis: YAxis,
412        values: &[f64],
413        labels: Option<&[&str]>,
414        keep_default: bool,
415    ) {
416        let cstrs: Option<Vec<std::ffi::CString>> = labels.map(|ls| {
417            ls.iter()
418                .map(|&s| std::ffi::CString::new(s).unwrap_or_default())
419                .collect()
420        });
421        if let Some(cstrs) = cstrs {
422            let raw: Vec<*const i8> = cstrs.iter().map(|c| c.as_ptr()).collect();
423            unsafe {
424                sys::ImPlot_SetupAxisTicks_doublePtr(
425                    axis as sys::ImAxis,
426                    values.as_ptr(),
427                    values.len() as i32,
428                    raw.as_ptr(),
429                    keep_default,
430                )
431            }
432        } else {
433            unsafe {
434                sys::ImPlot_SetupAxisTicks_doublePtr(
435                    axis as sys::ImAxis,
436                    values.as_ptr(),
437                    values.len() as i32,
438                    std::ptr::null(),
439                    keep_default,
440                )
441            }
442        }
443    }
444
445    /// Setup ticks on a range with tick count and optional labels for an X axis
446    pub fn setup_x_axis_ticks_range(
447        &self,
448        axis: XAxis,
449        v_min: f64,
450        v_max: f64,
451        n_ticks: i32,
452        labels: Option<&[&str]>,
453        keep_default: bool,
454    ) {
455        let cstrs: Option<Vec<std::ffi::CString>> = labels.map(|ls| {
456            ls.iter()
457                .map(|&s| std::ffi::CString::new(s).unwrap_or_default())
458                .collect()
459        });
460        if let Some(cstrs) = cstrs {
461            let raw: Vec<*const i8> = cstrs.iter().map(|c| c.as_ptr()).collect();
462            unsafe {
463                sys::ImPlot_SetupAxisTicks_double(
464                    axis as sys::ImAxis,
465                    v_min,
466                    v_max,
467                    n_ticks,
468                    raw.as_ptr(),
469                    keep_default,
470                )
471            }
472        } else {
473            unsafe {
474                sys::ImPlot_SetupAxisTicks_double(
475                    axis as sys::ImAxis,
476                    v_min,
477                    v_max,
478                    n_ticks,
479                    std::ptr::null(),
480                    keep_default,
481                )
482            }
483        }
484    }
485
486    /// Setup ticks on a range with tick count and optional labels for a Y axis
487    pub fn setup_y_axis_ticks_range(
488        &self,
489        axis: YAxis,
490        v_min: f64,
491        v_max: f64,
492        n_ticks: i32,
493        labels: Option<&[&str]>,
494        keep_default: bool,
495    ) {
496        let cstrs: Option<Vec<std::ffi::CString>> = labels.map(|ls| {
497            ls.iter()
498                .map(|&s| std::ffi::CString::new(s).unwrap_or_default())
499                .collect()
500        });
501        if let Some(cstrs) = cstrs {
502            let raw: Vec<*const i8> = cstrs.iter().map(|c| c.as_ptr()).collect();
503            unsafe {
504                sys::ImPlot_SetupAxisTicks_double(
505                    axis as sys::ImAxis,
506                    v_min,
507                    v_max,
508                    n_ticks,
509                    raw.as_ptr(),
510                    keep_default,
511                )
512            }
513        } else {
514            unsafe {
515                sys::ImPlot_SetupAxisTicks_double(
516                    axis as sys::ImAxis,
517                    v_min,
518                    v_max,
519                    n_ticks,
520                    std::ptr::null(),
521                    keep_default,
522                )
523            }
524        }
525    }
526
527    /// Setup tick label format string for a specific X axis
528    pub fn setup_x_axis_format(&self, axis: XAxis, fmt: &str) {
529        if let Ok(c) = std::ffi::CString::new(fmt) {
530            unsafe { sys::ImPlot_SetupAxisFormat_Str(axis as sys::ImAxis, c.as_ptr()) }
531        }
532    }
533
534    /// Setup tick label format string for a specific Y axis
535    pub fn setup_y_axis_format(&self, axis: YAxis, fmt: &str) {
536        if let Ok(c) = std::ffi::CString::new(fmt) {
537            unsafe { sys::ImPlot_SetupAxisFormat_Str(axis as sys::ImAxis, c.as_ptr()) }
538        }
539    }
540
541    /// Setup scale for a specific X axis (pass sys::ImPlotScale variant)
542    pub fn setup_x_axis_scale(&self, axis: XAxis, scale: sys::ImPlotScale) {
543        unsafe { sys::ImPlot_SetupAxisScale_PlotScale(axis as sys::ImAxis, scale) }
544    }
545
546    /// Setup scale for a specific Y axis (pass sys::ImPlotScale variant)
547    pub fn setup_y_axis_scale(&self, axis: YAxis, scale: sys::ImPlotScale) {
548        unsafe { sys::ImPlot_SetupAxisScale_PlotScale(axis as sys::ImAxis, scale) }
549    }
550
551    /// Setup axis limits constraints
552    pub fn setup_axis_limits_constraints(&self, axis: i32, v_min: f64, v_max: f64) {
553        unsafe { sys::ImPlot_SetupAxisLimitsConstraints(axis as sys::ImAxis, v_min, v_max) }
554    }
555
556    /// Setup axis zoom constraints
557    pub fn setup_axis_zoom_constraints(&self, axis: i32, z_min: f64, z_max: f64) {
558        unsafe { sys::ImPlot_SetupAxisZoomConstraints(axis as sys::ImAxis, z_min, z_max) }
559    }
560
561    // -------- Formatter (closure) --------
562    /// Setup tick label formatter using a Rust closure (lives until token drop)
563    pub fn setup_x_axis_format_closure<F>(&self, axis: XAxis, f: F) -> AxisFormatterToken
564    where
565        F: Fn(f64) -> String + Send + Sync + 'static,
566    {
567        AxisFormatterToken::new(axis as sys::ImAxis, f)
568    }
569
570    /// Setup tick label formatter using a Rust closure (lives until token drop)
571    pub fn setup_y_axis_format_closure<F>(&self, axis: YAxis, f: F) -> AxisFormatterToken
572    where
573        F: Fn(f64) -> String + Send + Sync + 'static,
574    {
575        AxisFormatterToken::new(axis as sys::ImAxis, f)
576    }
577
578    // -------- Transform (closure) --------
579    /// Setup custom axis transform using Rust closures (forward/inverse) valid until token drop
580    pub fn setup_x_axis_transform_closure<FW, INV>(
581        &self,
582        axis: XAxis,
583        forward: FW,
584        inverse: INV,
585    ) -> AxisTransformToken
586    where
587        FW: Fn(f64) -> f64 + Send + Sync + 'static,
588        INV: Fn(f64) -> f64 + Send + Sync + 'static,
589    {
590        AxisTransformToken::new(axis as sys::ImAxis, forward, inverse)
591    }
592
593    /// Setup custom axis transform for Y axis using closures
594    pub fn setup_y_axis_transform_closure<FW, INV>(
595        &self,
596        axis: YAxis,
597        forward: FW,
598        inverse: INV,
599    ) -> AxisTransformToken
600    where
601        FW: Fn(f64) -> f64 + Send + Sync + 'static,
602        INV: Fn(f64) -> f64 + Send + Sync + 'static,
603    {
604        AxisTransformToken::new(axis as sys::ImAxis, forward, inverse)
605    }
606}
607
608// =================== Formatter bridge ===================
609
610struct FormatterHolder {
611    func: Box<dyn Fn(f64) -> String + Send + Sync + 'static>,
612}
613
614pub struct AxisFormatterToken {
615    holder: Box<FormatterHolder>,
616    axis: sys::ImAxis,
617}
618
619impl AxisFormatterToken {
620    fn new<F>(axis: sys::ImAxis, f: F) -> Self
621    where
622        F: Fn(f64) -> String + Send + Sync + 'static,
623    {
624        let holder = Box::new(FormatterHolder { func: Box::new(f) });
625        let user = &*holder as *const FormatterHolder as *mut std::os::raw::c_void;
626        unsafe {
627            sys::ImPlot_SetupAxisFormat_PlotFormatter(
628                axis as sys::ImAxis,
629                Some(formatter_thunk),
630                user,
631            )
632        }
633        Self { holder, axis }
634    }
635}
636
637impl Drop for AxisFormatterToken {
638    fn drop(&mut self) {
639        // No explicit reset API; leaving plot scope ends usage. Holder drop frees closure.
640    }
641}
642
643unsafe extern "C" fn formatter_thunk(
644    value: f64,
645    buff: *mut std::os::raw::c_char,
646    size: std::os::raw::c_int,
647    user_data: *mut std::os::raw::c_void,
648) -> std::os::raw::c_int {
649    if user_data.is_null() || buff.is_null() || size <= 0 {
650        return 0;
651    }
652    let holder = unsafe { &*(user_data as *const FormatterHolder) };
653    let s = (holder.func)(value);
654    let bytes = s.as_bytes();
655    let max = (size - 1).max(0) as usize;
656    let n = bytes.len().min(max);
657    std::ptr::copy_nonoverlapping(bytes.as_ptr(), buff as *mut u8, n);
658    *buff.add(n) = 0;
659    n as std::os::raw::c_int
660}
661
662// =================== Transform bridge ===================
663
664struct TransformHolder {
665    forward: Box<dyn Fn(f64) -> f64 + Send + Sync + 'static>,
666    inverse: Box<dyn Fn(f64) -> f64 + Send + Sync + 'static>,
667}
668
669pub struct AxisTransformToken {
670    holder: Box<TransformHolder>,
671    axis: sys::ImAxis,
672}
673
674impl AxisTransformToken {
675    fn new<FW, INV>(axis: sys::ImAxis, forward: FW, inverse: INV) -> Self
676    where
677        FW: Fn(f64) -> f64 + Send + Sync + 'static,
678        INV: Fn(f64) -> f64 + Send + Sync + 'static,
679    {
680        let holder = Box::new(TransformHolder {
681            forward: Box::new(forward),
682            inverse: Box::new(inverse),
683        });
684        let user = &*holder as *const TransformHolder as *mut std::os::raw::c_void;
685        unsafe {
686            sys::ImPlot_SetupAxisScale_PlotTransform(
687                axis as sys::ImAxis,
688                Some(transform_forward_thunk),
689                Some(transform_inverse_thunk),
690                user,
691            )
692        }
693        Self { holder, axis }
694    }
695}
696
697impl Drop for AxisTransformToken {
698    fn drop(&mut self) {
699        // No explicit reset; scope end ends usage.
700    }
701}
702
703unsafe extern "C" fn transform_forward_thunk(
704    value: f64,
705    user_data: *mut std::os::raw::c_void,
706) -> f64 {
707    let holder = unsafe { &*(user_data as *const TransformHolder) };
708    (holder.forward)(value)
709}
710
711unsafe extern "C" fn transform_inverse_thunk(
712    value: f64,
713    user_data: *mut std::os::raw::c_void,
714) -> f64 {
715    let holder = unsafe { &*(user_data as *const TransformHolder) };
716    (holder.inverse)(value)
717}
718
719/// Token that represents an active plot
720///
721/// The plot will be automatically ended when this token is dropped.
722pub struct PlotToken<'ui> {
723    _lifetime: std::marker::PhantomData<&'ui ()>,
724}
725
726impl<'ui> PlotToken<'ui> {
727    /// Create a new PlotToken (internal use only)
728    pub(crate) fn new() -> Self {
729        Self {
730            _lifetime: std::marker::PhantomData,
731        }
732    }
733
734    /// Manually end the plot
735    ///
736    /// This is called automatically when the token is dropped,
737    /// but you can call it manually if needed.
738    pub fn end(self) {
739        // The actual ending happens in Drop
740    }
741}
742
743impl<'ui> Drop for PlotToken<'ui> {
744    fn drop(&mut self) {
745        unsafe {
746            sys::ImPlot_EndPlot();
747        }
748    }
749}