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 { 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 {
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        let mut out = sys::ImPlotPoint { x: 0.0, y: 0.0 };
201        unsafe {
202            sys::ImPlot_GetPlotMousePos(&mut out as *mut sys::ImPlotPoint, 0, y_axis);
203        }
204        out
205    }
206
207    /// Get the mouse position in plot coordinates for specific axes
208    pub fn get_plot_mouse_pos_axes(&self, x_axis: XAxis, y_axis: YAxis) -> sys::ImPlotPoint {
209        let mut out = sys::ImPlotPoint { x: 0.0, y: 0.0 };
210        unsafe {
211            sys::ImPlot_GetPlotMousePos(
212                &mut out as *mut sys::ImPlotPoint,
213                x_axis as i32,
214                y_axis as i32,
215            )
216        };
217        out
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_cstr = label.and_then(|s| std::ffi::CString::new(s).ok());
228        let ptr = label_cstr
229            .as_ref()
230            .map(|c| c.as_ptr())
231            .unwrap_or(std::ptr::null());
232        unsafe {
233            sys::ImPlot_SetupAxis(
234                axis as sys::ImAxis,
235                ptr,
236                flags.bits() as sys::ImPlotAxisFlags,
237            )
238        }
239    }
240
241    /// Setup a specific Y axis
242    pub fn setup_y_axis(&self, axis: YAxis, label: Option<&str>, flags: AxisFlags) {
243        let label_cstr = label.and_then(|s| std::ffi::CString::new(s).ok());
244        let ptr = label_cstr
245            .as_ref()
246            .map(|c| c.as_ptr())
247            .unwrap_or(std::ptr::null());
248        unsafe {
249            sys::ImPlot_SetupAxis(
250                axis as sys::ImAxis,
251                ptr,
252                flags.bits() as sys::ImPlotAxisFlags,
253            )
254        }
255    }
256
257    /// Setup axis limits for a specific X axis
258    pub fn setup_x_axis_limits(&self, axis: XAxis, min: f64, max: f64, cond: PlotCond) {
259        unsafe {
260            sys::ImPlot_SetupAxisLimits(axis as sys::ImAxis, min, max, cond as sys::ImPlotCond)
261        }
262    }
263
264    /// Setup axis limits for a specific Y axis
265    pub fn setup_y_axis_limits(&self, axis: YAxis, min: f64, max: f64, cond: PlotCond) {
266        unsafe {
267            sys::ImPlot_SetupAxisLimits(axis as sys::ImAxis, min, max, cond as sys::ImPlotCond)
268        }
269    }
270
271    /// Link an axis to external min/max values (live binding)
272    pub fn setup_axis_links(
273        &self,
274        axis: i32,
275        link_min: Option<&mut f64>,
276        link_max: Option<&mut f64>,
277    ) {
278        let pmin = link_min.map_or(std::ptr::null_mut(), |r| r as *mut f64);
279        let pmax = link_max.map_or(std::ptr::null_mut(), |r| r as *mut f64);
280        unsafe { sys::ImPlot_SetupAxisLinks(axis, pmin, pmax) }
281    }
282
283    /// Setup both axes labels/flags at once
284    pub fn setup_axes(
285        &self,
286        x_label: Option<&str>,
287        y_label: Option<&str>,
288        x_flags: AxisFlags,
289        y_flags: AxisFlags,
290    ) {
291        let x_c = x_label.and_then(|s| std::ffi::CString::new(s).ok());
292        let y_c = y_label.and_then(|s| std::ffi::CString::new(s).ok());
293        let xp = x_c.as_ref().map(|c| c.as_ptr()).unwrap_or(std::ptr::null());
294        let yp = y_c.as_ref().map(|c| c.as_ptr()).unwrap_or(std::ptr::null());
295        unsafe {
296            sys::ImPlot_SetupAxes(
297                xp,
298                yp,
299                x_flags.bits() as sys::ImPlotAxisFlags,
300                y_flags.bits() as sys::ImPlotAxisFlags,
301            )
302        }
303    }
304
305    /// Setup axes limits (both) at once
306    pub fn setup_axes_limits(
307        &self,
308        x_min: f64,
309        x_max: f64,
310        y_min: f64,
311        y_max: f64,
312        cond: PlotCond,
313    ) {
314        unsafe { sys::ImPlot_SetupAxesLimits(x_min, x_max, y_min, y_max, cond as sys::ImPlotCond) }
315    }
316
317    /// Call after axis setup to finalize configuration
318    pub fn setup_finish(&self) {
319        unsafe { sys::ImPlot_SetupFinish() }
320    }
321
322    /// Set next frame limits for a specific axis
323    pub fn set_next_x_axis_limits(&self, axis: XAxis, min: f64, max: f64, cond: PlotCond) {
324        unsafe {
325            sys::ImPlot_SetNextAxisLimits(axis as sys::ImAxis, min, max, cond as sys::ImPlotCond)
326        }
327    }
328
329    /// Set next frame limits for a specific axis
330    pub fn set_next_y_axis_limits(&self, axis: YAxis, min: f64, max: f64, cond: PlotCond) {
331        unsafe {
332            sys::ImPlot_SetNextAxisLimits(axis as sys::ImAxis, min, max, cond as sys::ImPlotCond)
333        }
334    }
335
336    /// Link an axis to external min/max for next frame
337    pub fn set_next_axis_links(
338        &self,
339        axis: i32,
340        link_min: Option<&mut f64>,
341        link_max: Option<&mut f64>,
342    ) {
343        let pmin = link_min.map_or(std::ptr::null_mut(), |r| r as *mut f64);
344        let pmax = link_max.map_or(std::ptr::null_mut(), |r| r as *mut f64);
345        unsafe { sys::ImPlot_SetNextAxisLinks(axis, pmin, pmax) }
346    }
347
348    /// Set next frame limits for both axes
349    pub fn set_next_axes_limits(
350        &self,
351        x_min: f64,
352        x_max: f64,
353        y_min: f64,
354        y_max: f64,
355        cond: PlotCond,
356    ) {
357        unsafe {
358            sys::ImPlot_SetNextAxesLimits(x_min, x_max, y_min, y_max, cond as sys::ImPlotCond)
359        }
360    }
361
362    /// Fit next frame both axes
363    pub fn set_next_axes_to_fit(&self) {
364        unsafe { sys::ImPlot_SetNextAxesToFit() }
365    }
366
367    /// Fit next frame a specific axis (raw)
368    pub fn set_next_axis_to_fit(&self, axis: i32) {
369        unsafe { sys::ImPlot_SetNextAxisToFit(axis as sys::ImAxis) }
370    }
371
372    /// Fit next frame a specific X axis
373    pub fn set_next_x_axis_to_fit(&self, axis: XAxis) {
374        unsafe { sys::ImPlot_SetNextAxisToFit(axis as sys::ImAxis) }
375    }
376
377    /// Fit next frame a specific Y axis
378    pub fn set_next_y_axis_to_fit(&self, axis: YAxis) {
379        unsafe { sys::ImPlot_SetNextAxisToFit(axis as sys::ImAxis) }
380    }
381
382    /// Setup ticks with explicit positions and optional labels for an X axis
383    pub fn setup_x_axis_ticks_positions(
384        &self,
385        axis: XAxis,
386        values: &[f64],
387        labels: Option<&[&str]>,
388        keep_default: bool,
389    ) {
390        let cstrs: Option<Vec<std::ffi::CString>> = labels.map(|ls| {
391            ls.iter()
392                .map(|&s| std::ffi::CString::new(s).unwrap_or_default())
393                .collect()
394        });
395        // To keep lifetimes, allocate a temp Vec if labels present
396        if let Some(vec) = cstrs {
397            let raw: Vec<*const i8> = vec.iter().map(|c| c.as_ptr()).collect();
398            unsafe {
399                sys::ImPlot_SetupAxisTicks_doublePtr(
400                    axis as sys::ImAxis,
401                    values.as_ptr(),
402                    values.len() as i32,
403                    raw.as_ptr(),
404                    keep_default,
405                )
406            }
407        } else {
408            unsafe {
409                sys::ImPlot_SetupAxisTicks_doublePtr(
410                    axis as sys::ImAxis,
411                    values.as_ptr(),
412                    values.len() as i32,
413                    std::ptr::null(),
414                    keep_default,
415                )
416            }
417        }
418    }
419
420    /// Setup ticks with explicit positions and optional labels for a Y axis
421    pub fn setup_y_axis_ticks_positions(
422        &self,
423        axis: YAxis,
424        values: &[f64],
425        labels: Option<&[&str]>,
426        keep_default: bool,
427    ) {
428        let cstrs: Option<Vec<std::ffi::CString>> = labels.map(|ls| {
429            ls.iter()
430                .map(|&s| std::ffi::CString::new(s).unwrap_or_default())
431                .collect()
432        });
433        if let Some(cstrs) = cstrs {
434            let raw: Vec<*const i8> = cstrs.iter().map(|c| c.as_ptr()).collect();
435            unsafe {
436                sys::ImPlot_SetupAxisTicks_doublePtr(
437                    axis as sys::ImAxis,
438                    values.as_ptr(),
439                    values.len() as i32,
440                    raw.as_ptr(),
441                    keep_default,
442                )
443            }
444        } else {
445            unsafe {
446                sys::ImPlot_SetupAxisTicks_doublePtr(
447                    axis as sys::ImAxis,
448                    values.as_ptr(),
449                    values.len() as i32,
450                    std::ptr::null(),
451                    keep_default,
452                )
453            }
454        }
455    }
456
457    /// Setup ticks on a range with tick count and optional labels for an X axis
458    pub fn setup_x_axis_ticks_range(
459        &self,
460        axis: XAxis,
461        v_min: f64,
462        v_max: f64,
463        n_ticks: i32,
464        labels: Option<&[&str]>,
465        keep_default: bool,
466    ) {
467        let cstrs: Option<Vec<std::ffi::CString>> = labels.map(|ls| {
468            ls.iter()
469                .map(|&s| std::ffi::CString::new(s).unwrap_or_default())
470                .collect()
471        });
472        if let Some(cstrs) = cstrs {
473            let raw: Vec<*const i8> = cstrs.iter().map(|c| c.as_ptr()).collect();
474            unsafe {
475                sys::ImPlot_SetupAxisTicks_double(
476                    axis as sys::ImAxis,
477                    v_min,
478                    v_max,
479                    n_ticks,
480                    raw.as_ptr(),
481                    keep_default,
482                )
483            }
484        } else {
485            unsafe {
486                sys::ImPlot_SetupAxisTicks_double(
487                    axis as sys::ImAxis,
488                    v_min,
489                    v_max,
490                    n_ticks,
491                    std::ptr::null(),
492                    keep_default,
493                )
494            }
495        }
496    }
497
498    /// Setup ticks on a range with tick count and optional labels for a Y axis
499    pub fn setup_y_axis_ticks_range(
500        &self,
501        axis: YAxis,
502        v_min: f64,
503        v_max: f64,
504        n_ticks: i32,
505        labels: Option<&[&str]>,
506        keep_default: bool,
507    ) {
508        let cstrs: Option<Vec<std::ffi::CString>> = labels.map(|ls| {
509            ls.iter()
510                .map(|&s| std::ffi::CString::new(s).unwrap_or_default())
511                .collect()
512        });
513        if let Some(cstrs) = cstrs {
514            let raw: Vec<*const i8> = cstrs.iter().map(|c| c.as_ptr()).collect();
515            unsafe {
516                sys::ImPlot_SetupAxisTicks_double(
517                    axis as sys::ImAxis,
518                    v_min,
519                    v_max,
520                    n_ticks,
521                    raw.as_ptr(),
522                    keep_default,
523                )
524            }
525        } else {
526            unsafe {
527                sys::ImPlot_SetupAxisTicks_double(
528                    axis as sys::ImAxis,
529                    v_min,
530                    v_max,
531                    n_ticks,
532                    std::ptr::null(),
533                    keep_default,
534                )
535            }
536        }
537    }
538
539    /// Setup tick label format string for a specific X axis
540    pub fn setup_x_axis_format(&self, axis: XAxis, fmt: &str) {
541        if let Ok(c) = std::ffi::CString::new(fmt) {
542            unsafe { sys::ImPlot_SetupAxisFormat_Str(axis as sys::ImAxis, c.as_ptr()) }
543        }
544    }
545
546    /// Setup tick label format string for a specific Y axis
547    pub fn setup_y_axis_format(&self, axis: YAxis, fmt: &str) {
548        if let Ok(c) = std::ffi::CString::new(fmt) {
549            unsafe { sys::ImPlot_SetupAxisFormat_Str(axis as sys::ImAxis, c.as_ptr()) }
550        }
551    }
552
553    /// Setup scale for a specific X axis (pass sys::ImPlotScale variant)
554    pub fn setup_x_axis_scale(&self, axis: XAxis, scale: sys::ImPlotScale) {
555        unsafe { sys::ImPlot_SetupAxisScale_PlotScale(axis as sys::ImAxis, scale) }
556    }
557
558    /// Setup scale for a specific Y axis (pass sys::ImPlotScale variant)
559    pub fn setup_y_axis_scale(&self, axis: YAxis, scale: sys::ImPlotScale) {
560        unsafe { sys::ImPlot_SetupAxisScale_PlotScale(axis as sys::ImAxis, scale) }
561    }
562
563    /// Setup axis limits constraints
564    pub fn setup_axis_limits_constraints(&self, axis: i32, v_min: f64, v_max: f64) {
565        unsafe { sys::ImPlot_SetupAxisLimitsConstraints(axis as sys::ImAxis, v_min, v_max) }
566    }
567
568    /// Setup axis zoom constraints
569    pub fn setup_axis_zoom_constraints(&self, axis: i32, z_min: f64, z_max: f64) {
570        unsafe { sys::ImPlot_SetupAxisZoomConstraints(axis as sys::ImAxis, z_min, z_max) }
571    }
572
573    // -------- Formatter (closure) --------
574    /// Setup tick label formatter using a Rust closure (lives until token drop)
575    pub fn setup_x_axis_format_closure<F>(&self, axis: XAxis, f: F) -> AxisFormatterToken
576    where
577        F: Fn(f64) -> String + Send + Sync + 'static,
578    {
579        AxisFormatterToken::new(axis as sys::ImAxis, f)
580    }
581
582    /// Setup tick label formatter using a Rust closure (lives until token drop)
583    pub fn setup_y_axis_format_closure<F>(&self, axis: YAxis, f: F) -> AxisFormatterToken
584    where
585        F: Fn(f64) -> String + Send + Sync + 'static,
586    {
587        AxisFormatterToken::new(axis as sys::ImAxis, f)
588    }
589
590    // -------- Transform (closure) --------
591    /// Setup custom axis transform using Rust closures (forward/inverse) valid until token drop
592    pub fn setup_x_axis_transform_closure<FW, INV>(
593        &self,
594        axis: XAxis,
595        forward: FW,
596        inverse: INV,
597    ) -> AxisTransformToken
598    where
599        FW: Fn(f64) -> f64 + Send + Sync + 'static,
600        INV: Fn(f64) -> f64 + Send + Sync + 'static,
601    {
602        AxisTransformToken::new(axis as sys::ImAxis, forward, inverse)
603    }
604
605    /// Setup custom axis transform for Y axis using closures
606    pub fn setup_y_axis_transform_closure<FW, INV>(
607        &self,
608        axis: YAxis,
609        forward: FW,
610        inverse: INV,
611    ) -> AxisTransformToken
612    where
613        FW: Fn(f64) -> f64 + Send + Sync + 'static,
614        INV: Fn(f64) -> f64 + Send + Sync + 'static,
615    {
616        AxisTransformToken::new(axis as sys::ImAxis, forward, inverse)
617    }
618}
619
620// =================== Formatter bridge ===================
621
622struct FormatterHolder {
623    func: Box<dyn Fn(f64) -> String + Send + Sync + 'static>,
624}
625
626pub struct AxisFormatterToken {
627    holder: Box<FormatterHolder>,
628    axis: sys::ImAxis,
629}
630
631impl AxisFormatterToken {
632    fn new<F>(axis: sys::ImAxis, f: F) -> Self
633    where
634        F: Fn(f64) -> String + Send + Sync + 'static,
635    {
636        let holder = Box::new(FormatterHolder { func: Box::new(f) });
637        let user = &*holder as *const FormatterHolder as *mut std::os::raw::c_void;
638        unsafe {
639            sys::ImPlot_SetupAxisFormat_PlotFormatter(
640                axis as sys::ImAxis,
641                Some(formatter_thunk),
642                user,
643            )
644        }
645        Self { holder, axis }
646    }
647}
648
649impl Drop for AxisFormatterToken {
650    fn drop(&mut self) {
651        // No explicit reset API; leaving plot scope ends usage. Holder drop frees closure.
652    }
653}
654
655unsafe extern "C" fn formatter_thunk(
656    value: f64,
657    buff: *mut std::os::raw::c_char,
658    size: std::os::raw::c_int,
659    user_data: *mut std::os::raw::c_void,
660) -> std::os::raw::c_int {
661    if user_data.is_null() || buff.is_null() || size <= 0 {
662        return 0;
663    }
664    let holder = unsafe { &*(user_data as *const FormatterHolder) };
665    let s = (holder.func)(value);
666    let bytes = s.as_bytes();
667    let max = (size - 1).max(0) as usize;
668    let n = bytes.len().min(max);
669    std::ptr::copy_nonoverlapping(bytes.as_ptr(), buff as *mut u8, n);
670    *buff.add(n) = 0;
671    n as std::os::raw::c_int
672}
673
674// =================== Transform bridge ===================
675
676struct TransformHolder {
677    forward: Box<dyn Fn(f64) -> f64 + Send + Sync + 'static>,
678    inverse: Box<dyn Fn(f64) -> f64 + Send + Sync + 'static>,
679}
680
681pub struct AxisTransformToken {
682    holder: Box<TransformHolder>,
683    axis: sys::ImAxis,
684}
685
686impl AxisTransformToken {
687    fn new<FW, INV>(axis: sys::ImAxis, forward: FW, inverse: INV) -> Self
688    where
689        FW: Fn(f64) -> f64 + Send + Sync + 'static,
690        INV: Fn(f64) -> f64 + Send + Sync + 'static,
691    {
692        let holder = Box::new(TransformHolder {
693            forward: Box::new(forward),
694            inverse: Box::new(inverse),
695        });
696        let user = &*holder as *const TransformHolder as *mut std::os::raw::c_void;
697        unsafe {
698            sys::ImPlot_SetupAxisScale_PlotTransform(
699                axis as sys::ImAxis,
700                Some(transform_forward_thunk),
701                Some(transform_inverse_thunk),
702                user,
703            )
704        }
705        Self { holder, axis }
706    }
707}
708
709impl Drop for AxisTransformToken {
710    fn drop(&mut self) {
711        // No explicit reset; scope end ends usage.
712    }
713}
714
715unsafe extern "C" fn transform_forward_thunk(
716    value: f64,
717    user_data: *mut std::os::raw::c_void,
718) -> f64 {
719    let holder = unsafe { &*(user_data as *const TransformHolder) };
720    (holder.forward)(value)
721}
722
723unsafe extern "C" fn transform_inverse_thunk(
724    value: f64,
725    user_data: *mut std::os::raw::c_void,
726) -> f64 {
727    let holder = unsafe { &*(user_data as *const TransformHolder) };
728    (holder.inverse)(value)
729}
730
731/// Token that represents an active plot
732///
733/// The plot will be automatically ended when this token is dropped.
734pub struct PlotToken<'ui> {
735    _lifetime: std::marker::PhantomData<&'ui ()>,
736}
737
738impl<'ui> PlotToken<'ui> {
739    /// Create a new PlotToken (internal use only)
740    pub(crate) fn new() -> Self {
741        Self {
742            _lifetime: std::marker::PhantomData,
743        }
744    }
745
746    /// Manually end the plot
747    ///
748    /// This is called automatically when the token is dropped,
749    /// but you can call it manually if needed.
750    pub fn end(self) {
751        // The actual ending happens in Drop
752    }
753}
754
755impl<'ui> Drop for PlotToken<'ui> {
756    fn drop(&mut self) {
757        unsafe {
758            sys::ImPlot_EndPlot();
759        }
760    }
761}