Skip to main content

dear_implot/
context.rs

1use crate::{Axis, 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
9fn assert_finite_f64(caller: &str, name: &str, value: f64) {
10    assert!(value.is_finite(), "{caller} {name} must be finite");
11}
12
13fn assert_finite_vec2(caller: &str, name: &str, value: [f32; 2]) {
14    assert!(
15        value[0].is_finite() && value[1].is_finite(),
16        "{caller} {name} must be finite"
17    );
18}
19
20fn assert_finite_f64_slice(caller: &str, name: &str, values: &[f64]) {
21    assert!(
22        values.iter().all(|value| value.is_finite()),
23        "{caller} {name} must contain only finite values"
24    );
25}
26
27fn assert_axis_limit_range(caller: &str, min: f64, max: f64) {
28    assert_finite_f64(caller, "min", min);
29    assert_finite_f64(caller, "max", max);
30    assert!(min != max, "{caller} min and max must differ");
31}
32
33fn assert_axis_constraint_range(caller: &str, min: f64, max: f64) {
34    assert_finite_f64(caller, "min", min);
35    assert_finite_f64(caller, "max", max);
36    assert!(min <= max, "{caller} min must be <= max");
37}
38
39fn assert_axis_zoom_range(caller: &str, min: f64, max: f64) {
40    assert_finite_f64(caller, "min", min);
41    assert_finite_f64(caller, "max", max);
42    assert!(min > 0.0, "{caller} min must be positive");
43    assert!(min <= max, "{caller} min must be <= max");
44}
45
46fn assert_positive_tick_count(caller: &str, n_ticks: i32) {
47    assert!(n_ticks > 0, "{caller} n_ticks must be positive");
48}
49
50/// ImPlot context that manages the plotting state
51///
52/// This context is separate from the Dear ImGui context but works alongside it.
53/// You need both contexts to create plots.
54pub struct PlotContext {
55    raw: *mut sys::ImPlotContext,
56    imgui_ctx_raw: *mut imgui_sys::ImGuiContext,
57    imgui_alive: Option<dear_imgui_rs::ContextAliveToken>,
58    owns_context: bool,
59}
60
61#[derive(Clone, Copy)]
62pub(crate) struct PlotContextBinding {
63    plot_ctx_raw: *mut sys::ImPlotContext,
64    imgui_ctx_raw: *mut imgui_sys::ImGuiContext,
65}
66
67impl PlotContextBinding {
68    pub(crate) fn bind(self, caller: &str) {
69        assert!(
70            !self.imgui_ctx_raw.is_null(),
71            "{caller} requires an active ImGui context"
72        );
73        assert!(
74            !self.plot_ctx_raw.is_null(),
75            "{caller} requires an active ImPlot context"
76        );
77        assert_eq!(
78            unsafe { imgui_sys::igGetCurrentContext() },
79            self.imgui_ctx_raw,
80            "{caller} must be used with the currently-active ImGui context"
81        );
82        unsafe {
83            sys::ImPlot_SetImGuiContext(self.imgui_ctx_raw);
84            sys::ImPlot_SetCurrentContext(self.plot_ctx_raw);
85        }
86    }
87}
88
89impl PlotContext {
90    /// Try to create a new ImPlot context
91    ///
92    /// This should be called after creating the Dear ImGui context.
93    /// The ImPlot context will use the same Dear ImGui context internally.
94    pub fn try_create(imgui_ctx: &ImGuiContext) -> dear_imgui_rs::ImGuiResult<Self> {
95        let imgui_ctx_raw = imgui_ctx.as_raw();
96        let imgui_alive = Some(imgui_ctx.alive_token());
97        assert_eq!(
98            unsafe { imgui_sys::igGetCurrentContext() },
99            imgui_ctx_raw,
100            "dear-implot: PlotContext must be created with the currently-active ImGui context"
101        );
102
103        // Bind ImPlot to the ImGui context before creating.
104        // On some toolchains/platforms, not setting this can lead to crashes
105        // if ImPlot initialization queries ImGui state during CreateContext.
106        unsafe { sys::ImPlot_SetImGuiContext(imgui_ctx_raw) };
107
108        let raw = unsafe { sys::ImPlot_CreateContext() };
109        if raw.is_null() {
110            return Err(dear_imgui_rs::ImGuiError::context_creation(
111                "ImPlot_CreateContext returned null",
112            ));
113        }
114
115        // Ensure the newly created context is current (defensive, CreateContext should do this).
116        unsafe { sys::ImPlot_SetCurrentContext(raw) };
117
118        Ok(Self {
119            raw,
120            imgui_ctx_raw,
121            imgui_alive,
122            owns_context: true,
123        })
124    }
125
126    /// Create a new ImPlot context (panics on error)
127    pub fn create(imgui_ctx: &ImGuiContext) -> Self {
128        Self::try_create(imgui_ctx).expect("Failed to create ImPlot context")
129    }
130
131    /// Get the current ImPlot context as a non-owning raw-context wrapper.
132    ///
133    /// Returns None if no context is current
134    ///
135    /// # Safety
136    ///
137    /// The returned value does not own the current ImPlot context and cannot prove that the
138    /// associated ImGui context remains alive. The caller must ensure the raw ImPlot and ImGui
139    /// contexts outlive the returned wrapper and are used on the same thread/context stack.
140    pub unsafe fn current() -> Option<Self> {
141        let raw = unsafe { sys::ImPlot_GetCurrentContext() };
142        if raw.is_null() {
143            None
144        } else {
145            Some(Self {
146                raw,
147                imgui_ctx_raw: unsafe { imgui_sys::igGetCurrentContext() },
148                imgui_alive: None,
149                owns_context: false,
150            })
151        }
152    }
153
154    /// Set this context as the current ImPlot context
155    pub fn set_as_current(&self) {
156        self.assert_imgui_alive();
157        self.binding().bind("dear-implot: PlotContext");
158    }
159
160    fn assert_imgui_alive(&self) {
161        if let Some(alive) = &self.imgui_alive {
162            assert!(
163                alive.is_alive(),
164                "dear-implot: ImGui context has been dropped"
165            );
166        }
167    }
168
169    fn binding(&self) -> PlotContextBinding {
170        PlotContextBinding {
171            plot_ctx_raw: self.raw,
172            imgui_ctx_raw: self.imgui_ctx_raw,
173        }
174    }
175
176    /// Get a PlotUi for creating plots
177    ///
178    /// This borrows both the ImPlot context and the Dear ImGui Ui,
179    /// ensuring that plots can only be created when both are available.
180    pub fn get_plot_ui<'ui>(&'ui self, ui: &'ui Ui) -> PlotUi<'ui> {
181        self.set_as_current();
182        PlotUi { context: self, ui }
183    }
184
185    /// Get the raw ImPlot context pointer
186    ///
187    /// # Safety
188    ///
189    /// The caller must ensure the pointer is used safely and not stored
190    /// beyond the lifetime of this context.
191    pub unsafe fn raw(&self) -> *mut sys::ImPlotContext {
192        self.raw
193    }
194}
195
196impl Drop for PlotContext {
197    fn drop(&mut self) {
198        if !self.owns_context || self.raw.is_null() {
199            return;
200        }
201
202        if let Some(alive) = &self.imgui_alive {
203            if !alive.is_alive() {
204                // Avoid calling into ImGui allocators after the context has been dropped.
205                // Best-effort: leak the ImPlot context instead of risking UB.
206                return;
207            }
208        }
209
210        unsafe {
211            let prev_imgui = imgui_sys::igGetCurrentContext();
212            imgui_sys::igSetCurrentContext(self.imgui_ctx_raw);
213            sys::ImPlot_SetImGuiContext(self.imgui_ctx_raw);
214
215            if sys::ImPlot_GetCurrentContext() == self.raw {
216                sys::ImPlot_SetCurrentContext(std::ptr::null_mut());
217            }
218            sys::ImPlot_DestroyContext(self.raw);
219
220            imgui_sys::igSetCurrentContext(prev_imgui);
221        }
222    }
223}
224
225// ImPlot context is tied to Dear ImGui and not thread-safe to send/share.
226
227/// A temporary reference for building plots
228///
229/// This struct ensures that plots can only be created when both ImGui and ImPlot
230/// contexts are available and properly set up.
231pub struct PlotUi<'ui> {
232    #[allow(dead_code)]
233    context: &'ui PlotContext,
234    #[allow(dead_code)]
235    ui: &'ui Ui,
236}
237
238impl<'ui> PlotUi<'ui> {
239    #[inline]
240    pub(crate) fn bind(&self) {
241        self.context.assert_imgui_alive();
242        self.context.binding().bind("dear-implot: PlotUi");
243    }
244
245    /// Begin a new plot with the given title
246    ///
247    /// Returns a PlotToken if the plot was successfully started.
248    /// The plot will be automatically ended when the token is dropped.
249    pub fn begin_plot(&self, title: &str) -> Option<PlotToken<'_>> {
250        let size = sys::ImVec2_c { x: -1.0, y: 0.0 };
251        if title.contains('\0') {
252            return None;
253        }
254        self.bind();
255        let started = with_scratch_txt(title, |ptr| unsafe { sys::ImPlot_BeginPlot(ptr, size, 0) });
256
257        if started {
258            Some(PlotToken::new(
259                self.context.binding(),
260                self.context.imgui_alive.clone(),
261            ))
262        } else {
263            None
264        }
265    }
266
267    /// Begin a plot with custom size
268    pub fn begin_plot_with_size(&self, title: &str, size: [f32; 2]) -> Option<PlotToken<'_>> {
269        assert_finite_vec2("PlotUi::begin_plot_with_size()", "size", size);
270        let plot_size = sys::ImVec2_c {
271            x: size[0],
272            y: size[1],
273        };
274        if title.contains('\0') {
275            return None;
276        }
277        self.bind();
278        let started = with_scratch_txt(title, |ptr| unsafe {
279            sys::ImPlot_BeginPlot(ptr, plot_size, 0)
280        });
281
282        if started {
283            Some(PlotToken::new(
284                self.context.binding(),
285                self.context.imgui_alive.clone(),
286            ))
287        } else {
288            None
289        }
290    }
291
292    /// Plot a line with the given label and data
293    ///
294    /// This is a convenience method that can be called within a plot.
295    pub fn plot_line(&self, label: &str, x_data: &[f64], y_data: &[f64]) {
296        if x_data.len() != y_data.len() {
297            return; // Data length mismatch
298        }
299        let count = match i32::try_from(x_data.len()) {
300            Ok(v) => v,
301            Err(_) => return,
302        };
303
304        let label = if label.contains('\0') { "" } else { label };
305        self.bind();
306        with_scratch_txt(label, |ptr| unsafe {
307            let spec = crate::plots::plot_spec_from(0, 0, std::mem::size_of::<f64>() as i32);
308            sys::ImPlot_PlotLine_doublePtrdoublePtr(
309                ptr,
310                x_data.as_ptr(),
311                y_data.as_ptr(),
312                count,
313                spec,
314            );
315        })
316    }
317
318    /// Plot a scatter plot with the given label and data
319    pub fn plot_scatter(&self, label: &str, x_data: &[f64], y_data: &[f64]) {
320        if x_data.len() != y_data.len() {
321            return; // Data length mismatch
322        }
323        let count = match i32::try_from(x_data.len()) {
324            Ok(v) => v,
325            Err(_) => return,
326        };
327
328        let label = if label.contains('\0') { "" } else { label };
329        self.bind();
330        with_scratch_txt(label, |ptr| unsafe {
331            let spec = crate::plots::plot_spec_from(0, 0, std::mem::size_of::<f64>() as i32);
332            sys::ImPlot_PlotScatter_doublePtrdoublePtr(
333                ptr,
334                x_data.as_ptr(),
335                y_data.as_ptr(),
336                count,
337                spec,
338            );
339        })
340    }
341
342    /// Plot a polygon with the given label and vertex data.
343    pub fn plot_polygon(&self, label: &str, x_data: &[f64], y_data: &[f64]) {
344        if x_data.len() != y_data.len() {
345            return;
346        }
347        let count = match i32::try_from(x_data.len()) {
348            Ok(v) => v,
349            Err(_) => return,
350        };
351
352        let label = if label.contains('\0') { "" } else { label };
353        self.bind();
354        with_scratch_txt(label, |ptr| unsafe {
355            let spec = crate::plots::plot_spec_from(0, 0, std::mem::size_of::<f64>() as i32);
356            sys::ImPlot_PlotPolygon_doublePtr(ptr, x_data.as_ptr(), y_data.as_ptr(), count, spec);
357        })
358    }
359
360    /// Check if the plot area is hovered
361    pub fn is_plot_hovered(&self) -> bool {
362        self.bind();
363        unsafe { sys::ImPlot_IsPlotHovered() }
364    }
365
366    /// Get the mouse position in plot coordinates
367    pub fn get_plot_mouse_pos(&self, y_axis: Option<crate::YAxisChoice>) -> sys::ImPlotPoint {
368        let y_axis_i32 = crate::y_axis_choice_option_to_i32(y_axis);
369        let y_axis = match y_axis_i32 {
370            0 => 3,
371            1 => 4,
372            2 => 5,
373            _ => 3,
374        };
375        self.bind();
376        unsafe { sys::ImPlot_GetPlotMousePos(0, y_axis) }
377    }
378
379    /// Get the mouse position in plot coordinates for specific axes
380    pub fn get_plot_mouse_pos_axes(&self, x_axis: XAxis, y_axis: YAxis) -> sys::ImPlotPoint {
381        self.bind();
382        unsafe { sys::ImPlot_GetPlotMousePos(x_axis as i32, y_axis as i32) }
383    }
384
385    /// Set current axes for subsequent plot submissions
386    pub fn set_axes(&self, x_axis: XAxis, y_axis: YAxis) {
387        self.bind();
388        unsafe { sys::ImPlot_SetAxes(x_axis as i32, y_axis as i32) }
389    }
390
391    /// Setup a specific X axis
392    pub fn setup_x_axis(&self, axis: XAxis, label: Option<&str>, flags: AxisFlags) {
393        self.bind();
394        let label = label.filter(|s| !s.contains('\0'));
395        match label {
396            Some(label) => with_scratch_txt(label, |ptr| unsafe {
397                sys::ImPlot_SetupAxis(
398                    axis as sys::ImAxis,
399                    ptr,
400                    flags.bits() as sys::ImPlotAxisFlags,
401                )
402            }),
403            None => unsafe {
404                sys::ImPlot_SetupAxis(
405                    axis as sys::ImAxis,
406                    std::ptr::null(),
407                    flags.bits() as sys::ImPlotAxisFlags,
408                )
409            },
410        }
411    }
412
413    /// Setup a specific Y axis
414    pub fn setup_y_axis(&self, axis: YAxis, label: Option<&str>, flags: AxisFlags) {
415        self.bind();
416        let label = label.filter(|s| !s.contains('\0'));
417        match label {
418            Some(label) => with_scratch_txt(label, |ptr| unsafe {
419                sys::ImPlot_SetupAxis(
420                    axis as sys::ImAxis,
421                    ptr,
422                    flags.bits() as sys::ImPlotAxisFlags,
423                )
424            }),
425            None => unsafe {
426                sys::ImPlot_SetupAxis(
427                    axis as sys::ImAxis,
428                    std::ptr::null(),
429                    flags.bits() as sys::ImPlotAxisFlags,
430                )
431            },
432        }
433    }
434
435    /// Setup axis limits for a specific X axis
436    pub fn setup_x_axis_limits(&self, axis: XAxis, min: f64, max: f64, cond: PlotCond) {
437        assert_axis_limit_range("PlotUi::setup_x_axis_limits()", min, max);
438        self.bind();
439        unsafe {
440            sys::ImPlot_SetupAxisLimits(axis as sys::ImAxis, min, max, cond as sys::ImPlotCond)
441        }
442    }
443
444    /// Setup axis limits for a specific Y axis
445    pub fn setup_y_axis_limits(&self, axis: YAxis, min: f64, max: f64, cond: PlotCond) {
446        assert_axis_limit_range("PlotUi::setup_y_axis_limits()", min, max);
447        self.bind();
448        unsafe {
449            sys::ImPlot_SetupAxisLimits(axis as sys::ImAxis, min, max, cond as sys::ImPlotCond)
450        }
451    }
452
453    /// Link an axis to external min/max values (live binding)
454    pub fn setup_axis_links(
455        &self,
456        axis: Axis,
457        link_min: Option<&mut f64>,
458        link_max: Option<&mut f64>,
459    ) {
460        let pmin = link_min.map_or(std::ptr::null_mut(), |r| r as *mut f64);
461        let pmax = link_max.map_or(std::ptr::null_mut(), |r| r as *mut f64);
462        self.bind();
463        unsafe { sys::ImPlot_SetupAxisLinks(axis.to_sys(), pmin, pmax) }
464    }
465
466    /// Link a raw axis to external min/max values (live binding).
467    ///
468    /// # Safety
469    ///
470    /// `axis` must be a valid ImPlot `ImAxis` value for the active plot. Passing an
471    /// out-of-range value lets ImPlot index internal axis arrays out of bounds.
472    pub unsafe fn setup_axis_links_unchecked(
473        &self,
474        axis: sys::ImAxis,
475        link_min: Option<&mut f64>,
476        link_max: Option<&mut f64>,
477    ) {
478        let pmin = link_min.map_or(std::ptr::null_mut(), |r| r as *mut f64);
479        let pmax = link_max.map_or(std::ptr::null_mut(), |r| r as *mut f64);
480        self.bind();
481        unsafe { sys::ImPlot_SetupAxisLinks(axis, pmin, pmax) }
482    }
483
484    /// Setup both axes labels/flags at once
485    pub fn setup_axes(
486        &self,
487        x_label: Option<&str>,
488        y_label: Option<&str>,
489        x_flags: AxisFlags,
490        y_flags: AxisFlags,
491    ) {
492        self.bind();
493        let x_label = x_label.filter(|s| !s.contains('\0'));
494        let y_label = y_label.filter(|s| !s.contains('\0'));
495
496        match (x_label, y_label) {
497            (Some(x_label), Some(y_label)) => {
498                with_scratch_txt_two(x_label, y_label, |xp, yp| unsafe {
499                    sys::ImPlot_SetupAxes(
500                        xp,
501                        yp,
502                        x_flags.bits() as sys::ImPlotAxisFlags,
503                        y_flags.bits() as sys::ImPlotAxisFlags,
504                    )
505                })
506            }
507            (Some(x_label), None) => with_scratch_txt(x_label, |xp| unsafe {
508                sys::ImPlot_SetupAxes(
509                    xp,
510                    std::ptr::null(),
511                    x_flags.bits() as sys::ImPlotAxisFlags,
512                    y_flags.bits() as sys::ImPlotAxisFlags,
513                )
514            }),
515            (None, Some(y_label)) => with_scratch_txt(y_label, |yp| unsafe {
516                sys::ImPlot_SetupAxes(
517                    std::ptr::null(),
518                    yp,
519                    x_flags.bits() as sys::ImPlotAxisFlags,
520                    y_flags.bits() as sys::ImPlotAxisFlags,
521                )
522            }),
523            (None, None) => unsafe {
524                sys::ImPlot_SetupAxes(
525                    std::ptr::null(),
526                    std::ptr::null(),
527                    x_flags.bits() as sys::ImPlotAxisFlags,
528                    y_flags.bits() as sys::ImPlotAxisFlags,
529                )
530            },
531        }
532    }
533
534    /// Setup axes limits (both) at once
535    pub fn setup_axes_limits(
536        &self,
537        x_min: f64,
538        x_max: f64,
539        y_min: f64,
540        y_max: f64,
541        cond: PlotCond,
542    ) {
543        assert_axis_limit_range("PlotUi::setup_axes_limits() x axis", x_min, x_max);
544        assert_axis_limit_range("PlotUi::setup_axes_limits() y axis", y_min, y_max);
545        self.bind();
546        unsafe { sys::ImPlot_SetupAxesLimits(x_min, x_max, y_min, y_max, cond as sys::ImPlotCond) }
547    }
548
549    /// Call after axis setup to finalize configuration
550    pub fn setup_finish(&self) {
551        self.bind();
552        unsafe { sys::ImPlot_SetupFinish() }
553    }
554
555    /// Set next frame limits for a specific axis
556    pub fn set_next_x_axis_limits(&self, axis: XAxis, min: f64, max: f64, cond: PlotCond) {
557        assert_axis_limit_range("PlotUi::set_next_x_axis_limits()", min, max);
558        self.bind();
559        unsafe {
560            sys::ImPlot_SetNextAxisLimits(axis as sys::ImAxis, min, max, cond as sys::ImPlotCond)
561        }
562    }
563
564    /// Set next frame limits for a specific axis
565    pub fn set_next_y_axis_limits(&self, axis: YAxis, min: f64, max: f64, cond: PlotCond) {
566        assert_axis_limit_range("PlotUi::set_next_y_axis_limits()", min, max);
567        self.bind();
568        unsafe {
569            sys::ImPlot_SetNextAxisLimits(axis as sys::ImAxis, min, max, cond as sys::ImPlotCond)
570        }
571    }
572
573    /// Link an axis to external min/max for next frame
574    pub fn set_next_axis_links(
575        &self,
576        axis: Axis,
577        link_min: Option<&mut f64>,
578        link_max: Option<&mut f64>,
579    ) {
580        let pmin = link_min.map_or(std::ptr::null_mut(), |r| r as *mut f64);
581        let pmax = link_max.map_or(std::ptr::null_mut(), |r| r as *mut f64);
582        self.bind();
583        unsafe { sys::ImPlot_SetNextAxisLinks(axis.to_sys(), pmin, pmax) }
584    }
585
586    /// Link a raw axis to external min/max for the next frame.
587    ///
588    /// # Safety
589    ///
590    /// `axis` must be a valid ImPlot `ImAxis` value. Passing an out-of-range
591    /// value lets ImPlot index internal next-plot arrays out of bounds.
592    pub unsafe fn set_next_axis_links_unchecked(
593        &self,
594        axis: sys::ImAxis,
595        link_min: Option<&mut f64>,
596        link_max: Option<&mut f64>,
597    ) {
598        let pmin = link_min.map_or(std::ptr::null_mut(), |r| r as *mut f64);
599        let pmax = link_max.map_or(std::ptr::null_mut(), |r| r as *mut f64);
600        self.bind();
601        unsafe { sys::ImPlot_SetNextAxisLinks(axis, pmin, pmax) }
602    }
603
604    /// Set next frame limits for both axes
605    pub fn set_next_axes_limits(
606        &self,
607        x_min: f64,
608        x_max: f64,
609        y_min: f64,
610        y_max: f64,
611        cond: PlotCond,
612    ) {
613        assert_axis_limit_range("PlotUi::set_next_axes_limits() x axis", x_min, x_max);
614        assert_axis_limit_range("PlotUi::set_next_axes_limits() y axis", y_min, y_max);
615        self.bind();
616        unsafe {
617            sys::ImPlot_SetNextAxesLimits(x_min, x_max, y_min, y_max, cond as sys::ImPlotCond)
618        }
619    }
620
621    /// Fit next frame both axes
622    pub fn set_next_axes_to_fit(&self) {
623        self.bind();
624        unsafe { sys::ImPlot_SetNextAxesToFit() }
625    }
626
627    /// Fit next frame a specific axis
628    pub fn set_next_axis_to_fit(&self, axis: Axis) {
629        self.bind();
630        unsafe { sys::ImPlot_SetNextAxisToFit(axis.to_sys()) }
631    }
632
633    /// Fit next frame a raw axis.
634    ///
635    /// # Safety
636    ///
637    /// `axis` must be a valid ImPlot `ImAxis` value. Passing an out-of-range
638    /// value lets ImPlot index internal next-plot arrays out of bounds.
639    pub unsafe fn set_next_axis_to_fit_unchecked(&self, axis: sys::ImAxis) {
640        self.bind();
641        unsafe { sys::ImPlot_SetNextAxisToFit(axis) }
642    }
643
644    /// Fit next frame a specific X axis
645    pub fn set_next_x_axis_to_fit(&self, axis: XAxis) {
646        self.bind();
647        unsafe { sys::ImPlot_SetNextAxisToFit(axis as sys::ImAxis) }
648    }
649
650    /// Fit next frame a specific Y axis
651    pub fn set_next_y_axis_to_fit(&self, axis: YAxis) {
652        self.bind();
653        unsafe { sys::ImPlot_SetNextAxisToFit(axis as sys::ImAxis) }
654    }
655
656    /// Setup ticks with explicit positions and optional labels for an X axis.
657    ///
658    /// If `labels` is provided, it must have the same length as `values`.
659    pub fn setup_x_axis_ticks_positions(
660        &self,
661        axis: XAxis,
662        values: &[f64],
663        labels: Option<&[&str]>,
664        keep_default: bool,
665    ) {
666        assert_finite_f64_slice("PlotUi::setup_x_axis_ticks_positions()", "values", values);
667        self.bind();
668        let count = match i32::try_from(values.len()) {
669            Ok(v) => v,
670            Err(_) => return,
671        };
672        if let Some(labels) = labels {
673            if labels.len() != values.len() {
674                return;
675            }
676            let cleaned: Vec<&str> = labels
677                .iter()
678                .map(|&s| if s.contains('\0') { "" } else { s })
679                .collect();
680            with_scratch_txt_slice(&cleaned, |ptrs| unsafe {
681                sys::ImPlot_SetupAxisTicks_doublePtr(
682                    axis as sys::ImAxis,
683                    values.as_ptr(),
684                    count,
685                    ptrs.as_ptr() as *const *const c_char,
686                    keep_default,
687                )
688            })
689        } else {
690            unsafe {
691                sys::ImPlot_SetupAxisTicks_doublePtr(
692                    axis as sys::ImAxis,
693                    values.as_ptr(),
694                    count,
695                    std::ptr::null(),
696                    keep_default,
697                )
698            }
699        }
700    }
701
702    /// Setup ticks with explicit positions and optional labels for a Y axis.
703    ///
704    /// If `labels` is provided, it must have the same length as `values`.
705    pub fn setup_y_axis_ticks_positions(
706        &self,
707        axis: YAxis,
708        values: &[f64],
709        labels: Option<&[&str]>,
710        keep_default: bool,
711    ) {
712        assert_finite_f64_slice("PlotUi::setup_y_axis_ticks_positions()", "values", values);
713        self.bind();
714        let count = match i32::try_from(values.len()) {
715            Ok(v) => v,
716            Err(_) => return,
717        };
718        if let Some(labels) = labels {
719            if labels.len() != values.len() {
720                return;
721            }
722            let cleaned: Vec<&str> = labels
723                .iter()
724                .map(|&s| if s.contains('\0') { "" } else { s })
725                .collect();
726            with_scratch_txt_slice(&cleaned, |ptrs| unsafe {
727                sys::ImPlot_SetupAxisTicks_doublePtr(
728                    axis as sys::ImAxis,
729                    values.as_ptr(),
730                    count,
731                    ptrs.as_ptr() as *const *const c_char,
732                    keep_default,
733                )
734            })
735        } else {
736            unsafe {
737                sys::ImPlot_SetupAxisTicks_doublePtr(
738                    axis as sys::ImAxis,
739                    values.as_ptr(),
740                    count,
741                    std::ptr::null(),
742                    keep_default,
743                )
744            }
745        }
746    }
747
748    /// Setup ticks on a range with tick count and optional labels for an X axis.
749    ///
750    /// If `labels` is provided, it must have length `n_ticks`.
751    pub fn setup_x_axis_ticks_range(
752        &self,
753        axis: XAxis,
754        v_min: f64,
755        v_max: f64,
756        n_ticks: i32,
757        labels: Option<&[&str]>,
758        keep_default: bool,
759    ) {
760        assert_axis_limit_range("PlotUi::setup_x_axis_ticks_range()", v_min, v_max);
761        assert_positive_tick_count("PlotUi::setup_x_axis_ticks_range()", n_ticks);
762        self.bind();
763        if let Some(labels) = labels {
764            let Ok(ticks_usize) = usize::try_from(n_ticks) else {
765                return;
766            };
767            if labels.len() != ticks_usize {
768                return;
769            }
770            let cleaned: Vec<&str> = labels
771                .iter()
772                .map(|&s| if s.contains('\0') { "" } else { s })
773                .collect();
774            with_scratch_txt_slice(&cleaned, |ptrs| unsafe {
775                sys::ImPlot_SetupAxisTicks_double(
776                    axis as sys::ImAxis,
777                    v_min,
778                    v_max,
779                    n_ticks,
780                    ptrs.as_ptr() as *const *const c_char,
781                    keep_default,
782                )
783            })
784        } else {
785            unsafe {
786                sys::ImPlot_SetupAxisTicks_double(
787                    axis as sys::ImAxis,
788                    v_min,
789                    v_max,
790                    n_ticks,
791                    std::ptr::null(),
792                    keep_default,
793                )
794            }
795        }
796    }
797
798    /// Setup ticks on a range with tick count and optional labels for a Y axis.
799    ///
800    /// If `labels` is provided, it must have length `n_ticks`.
801    pub fn setup_y_axis_ticks_range(
802        &self,
803        axis: YAxis,
804        v_min: f64,
805        v_max: f64,
806        n_ticks: i32,
807        labels: Option<&[&str]>,
808        keep_default: bool,
809    ) {
810        assert_axis_limit_range("PlotUi::setup_y_axis_ticks_range()", v_min, v_max);
811        assert_positive_tick_count("PlotUi::setup_y_axis_ticks_range()", n_ticks);
812        self.bind();
813        if let Some(labels) = labels {
814            let Ok(ticks_usize) = usize::try_from(n_ticks) else {
815                return;
816            };
817            if labels.len() != ticks_usize {
818                return;
819            }
820            let cleaned: Vec<&str> = labels
821                .iter()
822                .map(|&s| if s.contains('\0') { "" } else { s })
823                .collect();
824            with_scratch_txt_slice(&cleaned, |ptrs| unsafe {
825                sys::ImPlot_SetupAxisTicks_double(
826                    axis as sys::ImAxis,
827                    v_min,
828                    v_max,
829                    n_ticks,
830                    ptrs.as_ptr() as *const *const c_char,
831                    keep_default,
832                )
833            })
834        } else {
835            unsafe {
836                sys::ImPlot_SetupAxisTicks_double(
837                    axis as sys::ImAxis,
838                    v_min,
839                    v_max,
840                    n_ticks,
841                    std::ptr::null(),
842                    keep_default,
843                )
844            }
845        }
846    }
847
848    /// Setup tick label format string for a specific X axis
849    pub fn setup_x_axis_format(&self, axis: XAxis, fmt: &str) {
850        if fmt.contains('\0') {
851            return;
852        }
853        self.bind();
854        with_scratch_txt(fmt, |ptr| unsafe {
855            sys::ImPlot_SetupAxisFormat_Str(axis as sys::ImAxis, ptr)
856        })
857    }
858
859    /// Setup tick label format string for a specific Y axis
860    pub fn setup_y_axis_format(&self, axis: YAxis, fmt: &str) {
861        if fmt.contains('\0') {
862            return;
863        }
864        self.bind();
865        with_scratch_txt(fmt, |ptr| unsafe {
866            sys::ImPlot_SetupAxisFormat_Str(axis as sys::ImAxis, ptr)
867        })
868    }
869
870    /// Setup scale for a specific X axis (pass sys::ImPlotScale variant)
871    pub fn setup_x_axis_scale(&self, axis: XAxis, scale: sys::ImPlotScale) {
872        self.bind();
873        unsafe { sys::ImPlot_SetupAxisScale_PlotScale(axis as sys::ImAxis, scale) }
874    }
875
876    /// Setup scale for a specific Y axis (pass sys::ImPlotScale variant)
877    pub fn setup_y_axis_scale(&self, axis: YAxis, scale: sys::ImPlotScale) {
878        self.bind();
879        unsafe { sys::ImPlot_SetupAxisScale_PlotScale(axis as sys::ImAxis, scale) }
880    }
881
882    /// Setup axis limits constraints
883    pub fn setup_axis_limits_constraints(&self, axis: Axis, v_min: f64, v_max: f64) {
884        assert_axis_constraint_range("PlotUi::setup_axis_limits_constraints()", v_min, v_max);
885        self.bind();
886        unsafe { sys::ImPlot_SetupAxisLimitsConstraints(axis.to_sys(), v_min, v_max) }
887    }
888
889    /// Setup raw axis limits constraints.
890    ///
891    /// # Safety
892    ///
893    /// `axis` must be a valid ImPlot `ImAxis` value for the active plot. Passing an
894    /// out-of-range value lets ImPlot index internal axis arrays out of bounds.
895    pub unsafe fn setup_axis_limits_constraints_unchecked(
896        &self,
897        axis: sys::ImAxis,
898        v_min: f64,
899        v_max: f64,
900    ) {
901        assert_axis_constraint_range(
902            "PlotUi::setup_axis_limits_constraints_unchecked()",
903            v_min,
904            v_max,
905        );
906        self.bind();
907        unsafe { sys::ImPlot_SetupAxisLimitsConstraints(axis, v_min, v_max) }
908    }
909
910    /// Setup axis zoom constraints
911    pub fn setup_axis_zoom_constraints(&self, axis: Axis, z_min: f64, z_max: f64) {
912        assert_axis_zoom_range("PlotUi::setup_axis_zoom_constraints()", z_min, z_max);
913        self.bind();
914        unsafe { sys::ImPlot_SetupAxisZoomConstraints(axis.to_sys(), z_min, z_max) }
915    }
916
917    /// Setup raw axis zoom constraints.
918    ///
919    /// # Safety
920    ///
921    /// `axis` must be a valid ImPlot `ImAxis` value for the active plot. Passing an
922    /// out-of-range value lets ImPlot index internal axis arrays out of bounds.
923    pub unsafe fn setup_axis_zoom_constraints_unchecked(
924        &self,
925        axis: sys::ImAxis,
926        z_min: f64,
927        z_max: f64,
928    ) {
929        assert_axis_zoom_range(
930            "PlotUi::setup_axis_zoom_constraints_unchecked()",
931            z_min,
932            z_max,
933        );
934        self.bind();
935        unsafe { sys::ImPlot_SetupAxisZoomConstraints(axis, z_min, z_max) }
936    }
937
938    // -------- Formatter (closure) --------
939    /// Setup tick label formatter using a Rust closure.
940    ///
941    /// The closure is kept alive until the current plot ends.
942    pub fn setup_x_axis_format_closure<F>(&self, axis: XAxis, f: F) -> AxisFormatterToken
943    where
944        F: Fn(f64) -> String + Send + Sync + 'static,
945    {
946        self.bind();
947        AxisFormatterToken::new(axis as sys::ImAxis, f)
948    }
949
950    /// Setup tick label formatter using a Rust closure.
951    ///
952    /// The closure is kept alive until the current plot ends.
953    pub fn setup_y_axis_format_closure<F>(&self, axis: YAxis, f: F) -> AxisFormatterToken
954    where
955        F: Fn(f64) -> String + Send + Sync + 'static,
956    {
957        self.bind();
958        AxisFormatterToken::new(axis as sys::ImAxis, f)
959    }
960
961    // -------- Transform (closure) --------
962    /// Setup custom axis transform using Rust closures (forward/inverse).
963    ///
964    /// The closures are kept alive until the current plot ends.
965    pub fn setup_x_axis_transform_closure<FW, INV>(
966        &self,
967        axis: XAxis,
968        forward: FW,
969        inverse: INV,
970    ) -> AxisTransformToken
971    where
972        FW: Fn(f64) -> f64 + Send + Sync + 'static,
973        INV: Fn(f64) -> f64 + Send + Sync + 'static,
974    {
975        self.bind();
976        AxisTransformToken::new(axis as sys::ImAxis, forward, inverse)
977    }
978
979    /// Setup custom axis transform for Y axis using closures
980    pub fn setup_y_axis_transform_closure<FW, INV>(
981        &self,
982        axis: YAxis,
983        forward: FW,
984        inverse: INV,
985    ) -> AxisTransformToken
986    where
987        FW: Fn(f64) -> f64 + Send + Sync + 'static,
988        INV: Fn(f64) -> f64 + Send + Sync + 'static,
989    {
990        self.bind();
991        AxisTransformToken::new(axis as sys::ImAxis, forward, inverse)
992    }
993}
994
995// Plot-scope callback storage -------------------------------------------------
996//
997// ImPlot's axis formatter/transform APIs take function pointers + `user_data`
998// pointers, and may call them at any point until the current plot ends.
999//
1000// Returning a standalone token that owns the closure is unsound: safe Rust code
1001// could drop the token early, leaving ImPlot with a dangling `user_data` pointer.
1002//
1003// To keep the safe API sound without forcing users to manually retain tokens,
1004// we store callback holders in thread-local, plot-scoped storage that is
1005// created when a plot begins and destroyed when the plot ends.
1006
1007#[derive(Default)]
1008struct PlotScopeStorage {
1009    formatters: Vec<Box<FormatterHolder>>,
1010    transforms: Vec<Box<TransformHolder>>,
1011}
1012
1013thread_local! {
1014    static PLOT_SCOPE_STACK: RefCell<Vec<PlotScopeStorage>> = const { RefCell::new(Vec::new()) };
1015}
1016
1017fn with_plot_scope_storage<T>(f: impl FnOnce(&mut PlotScopeStorage) -> T) -> Option<T> {
1018    PLOT_SCOPE_STACK.with(|stack| {
1019        let mut stack = stack.borrow_mut();
1020        stack.last_mut().map(f)
1021    })
1022}
1023
1024pub(crate) struct PlotScopeGuard {
1025    _not_send_or_sync: std::marker::PhantomData<Rc<()>>,
1026}
1027
1028impl PlotScopeGuard {
1029    pub(crate) fn new() -> Self {
1030        PLOT_SCOPE_STACK.with(|stack| stack.borrow_mut().push(PlotScopeStorage::default()));
1031        Self {
1032            _not_send_or_sync: std::marker::PhantomData,
1033        }
1034    }
1035}
1036
1037impl Drop for PlotScopeGuard {
1038    fn drop(&mut self) {
1039        PLOT_SCOPE_STACK.with(|stack| {
1040            let popped = stack.borrow_mut().pop();
1041            debug_assert!(popped.is_some(), "dear-implot: plot scope stack underflow");
1042        });
1043    }
1044}
1045
1046// =================== Formatter bridge ===================
1047
1048struct FormatterHolder {
1049    func: Box<dyn Fn(f64) -> String + Send + Sync + 'static>,
1050}
1051
1052#[must_use]
1053pub struct AxisFormatterToken {
1054    _private: (),
1055}
1056
1057impl AxisFormatterToken {
1058    fn new<F>(axis: sys::ImAxis, f: F) -> Self
1059    where
1060        F: Fn(f64) -> String + Send + Sync + 'static,
1061    {
1062        let configured = with_plot_scope_storage(|storage| {
1063            let holder = Box::new(FormatterHolder { func: Box::new(f) });
1064            let user = &*holder as *const FormatterHolder as *mut std::os::raw::c_void;
1065            storage.formatters.push(holder);
1066            unsafe {
1067                sys::ImPlot_SetupAxisFormat_PlotFormatter(
1068                    axis as sys::ImAxis,
1069                    Some(formatter_thunk),
1070                    user,
1071                )
1072            }
1073        })
1074        .is_some();
1075
1076        debug_assert!(
1077            configured,
1078            "dear-implot: axis formatter closure must be set within an active plot"
1079        );
1080
1081        Self { _private: () }
1082    }
1083}
1084
1085impl Drop for AxisFormatterToken {
1086    fn drop(&mut self) {
1087        // The actual callback lifetime is managed by PlotScopeGuard.
1088    }
1089}
1090
1091unsafe extern "C" fn formatter_thunk(
1092    value: f64,
1093    buff: *mut std::os::raw::c_char,
1094    size: std::os::raw::c_int,
1095    user_data: *mut std::os::raw::c_void,
1096) -> std::os::raw::c_int {
1097    if user_data.is_null() || buff.is_null() || size <= 0 {
1098        return 0;
1099    }
1100    // Safety: ImPlot passes back the same pointer we provided in `AxisFormatterToken::new`.
1101    let holder = unsafe { &*(user_data as *const FormatterHolder) };
1102    let s = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (holder.func)(value))) {
1103        Ok(v) => v,
1104        Err(_) => {
1105            eprintln!("dear-implot: panic in axis formatter callback");
1106            std::process::abort();
1107        }
1108    };
1109    let bytes = s.as_bytes();
1110    let max = (size - 1).max(0) as usize;
1111    let n = bytes.len().min(max);
1112
1113    // Safety: `buff` is assumed to point to a valid buffer of at least `size`
1114    // bytes, with space for a terminating null. This matches ImPlot's
1115    // formatter contract.
1116    unsafe {
1117        std::ptr::copy_nonoverlapping(bytes.as_ptr(), buff as *mut u8, n);
1118        *buff.add(n) = 0;
1119    }
1120    n as std::os::raw::c_int
1121}
1122
1123// =================== Transform bridge ===================
1124
1125struct TransformHolder {
1126    forward: Box<dyn Fn(f64) -> f64 + Send + Sync + 'static>,
1127    inverse: Box<dyn Fn(f64) -> f64 + Send + Sync + 'static>,
1128}
1129
1130#[must_use]
1131pub struct AxisTransformToken {
1132    _private: (),
1133}
1134
1135impl AxisTransformToken {
1136    fn new<FW, INV>(axis: sys::ImAxis, forward: FW, inverse: INV) -> Self
1137    where
1138        FW: Fn(f64) -> f64 + Send + Sync + 'static,
1139        INV: Fn(f64) -> f64 + Send + Sync + 'static,
1140    {
1141        let configured = with_plot_scope_storage(|storage| {
1142            let holder = Box::new(TransformHolder {
1143                forward: Box::new(forward),
1144                inverse: Box::new(inverse),
1145            });
1146            let user = &*holder as *const TransformHolder as *mut std::os::raw::c_void;
1147            storage.transforms.push(holder);
1148            unsafe {
1149                sys::ImPlot_SetupAxisScale_PlotTransform(
1150                    axis as sys::ImAxis,
1151                    Some(transform_forward_thunk),
1152                    Some(transform_inverse_thunk),
1153                    user,
1154                )
1155            }
1156        })
1157        .is_some();
1158
1159        debug_assert!(
1160            configured,
1161            "dear-implot: axis transform closure must be set within an active plot"
1162        );
1163
1164        Self { _private: () }
1165    }
1166}
1167
1168impl Drop for AxisTransformToken {
1169    fn drop(&mut self) {
1170        // The actual callback lifetime is managed by PlotScopeGuard.
1171    }
1172}
1173
1174unsafe extern "C" fn transform_forward_thunk(
1175    value: f64,
1176    user_data: *mut std::os::raw::c_void,
1177) -> f64 {
1178    if user_data.is_null() {
1179        return value;
1180    }
1181    let holder = unsafe { &*(user_data as *const TransformHolder) };
1182    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (holder.forward)(value))) {
1183        Ok(v) => v,
1184        Err(_) => {
1185            eprintln!("dear-implot: panic in axis transform (forward) callback");
1186            std::process::abort();
1187        }
1188    }
1189}
1190
1191unsafe extern "C" fn transform_inverse_thunk(
1192    value: f64,
1193    user_data: *mut std::os::raw::c_void,
1194) -> f64 {
1195    if user_data.is_null() {
1196        return value;
1197    }
1198    let holder = unsafe { &*(user_data as *const TransformHolder) };
1199    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (holder.inverse)(value))) {
1200        Ok(v) => v,
1201        Err(_) => {
1202            eprintln!("dear-implot: panic in axis transform (inverse) callback");
1203            std::process::abort();
1204        }
1205    }
1206}
1207
1208/// Token that represents an active plot
1209///
1210/// The plot will be automatically ended when this token is dropped.
1211pub struct PlotToken<'ui> {
1212    binding: PlotContextBinding,
1213    imgui_alive: Option<dear_imgui_rs::ContextAliveToken>,
1214    _scope: PlotScopeGuard,
1215    _lifetime: std::marker::PhantomData<&'ui ()>,
1216}
1217
1218impl<'ui> PlotToken<'ui> {
1219    /// Create a new PlotToken (internal use only)
1220    pub(crate) fn new(
1221        binding: PlotContextBinding,
1222        imgui_alive: Option<dear_imgui_rs::ContextAliveToken>,
1223    ) -> Self {
1224        Self {
1225            binding,
1226            imgui_alive,
1227            _scope: PlotScopeGuard::new(),
1228            _lifetime: std::marker::PhantomData,
1229        }
1230    }
1231
1232    /// Manually end the plot
1233    ///
1234    /// This is called automatically when the token is dropped,
1235    /// but you can call it manually if needed.
1236    pub fn end(self) {
1237        // The actual ending happens in Drop
1238    }
1239}
1240
1241impl<'ui> Drop for PlotToken<'ui> {
1242    fn drop(&mut self) {
1243        if let Some(alive) = &self.imgui_alive {
1244            assert!(
1245                alive.is_alive(),
1246                "dear-implot: ImGui context has been dropped"
1247            );
1248        }
1249        self.binding.bind("dear-implot: PlotToken");
1250        unsafe {
1251            sys::ImPlot_EndPlot();
1252        }
1253    }
1254}
1255
1256#[cfg(test)]
1257mod tests {
1258    use super::{PlotContext, sys};
1259    use crate::{Axis, PlotCond, XAxis, YAxis};
1260    use dear_imgui_rs::{BackendFlags, Context};
1261    use std::sync::{Mutex, OnceLock};
1262
1263    fn test_guard() -> std::sync::MutexGuard<'static, ()> {
1264        static GUARD: OnceLock<Mutex<()>> = OnceLock::new();
1265        GUARD.get_or_init(|| Mutex::new(())).lock().unwrap()
1266    }
1267
1268    fn prepare_imgui(imgui: &mut Context) {
1269        let io = imgui.io_mut();
1270        io.set_display_size([800.0, 600.0]);
1271        io.set_delta_time(1.0 / 60.0);
1272        io.set_backend_flags(io.backend_flags() | BackendFlags::RENDERER_HAS_TEXTURES);
1273    }
1274
1275    #[test]
1276    fn plot_ui_binds_own_context_before_calls() {
1277        let _guard = test_guard();
1278        let mut imgui = Context::create();
1279        prepare_imgui(&mut imgui);
1280        let plot_a = PlotContext::create(&imgui);
1281        let raw_a = plot_a.raw;
1282        let plot_b = PlotContext::create(&imgui);
1283        let raw_b = plot_b.raw;
1284
1285        {
1286            let ui = imgui.frame();
1287            let plot_ui = plot_a.get_plot_ui(&ui);
1288            unsafe { sys::ImPlot_SetCurrentContext(raw_b) };
1289
1290            plot_ui.set_next_axes_to_fit();
1291
1292            assert_eq!(unsafe { sys::ImPlot_GetCurrentContext() }, raw_a);
1293        }
1294        let _ = imgui.render();
1295
1296        drop(plot_b);
1297        drop(plot_a);
1298    }
1299
1300    #[test]
1301    fn plot_token_binds_own_context_before_drop() {
1302        let _guard = test_guard();
1303        let mut imgui = Context::create();
1304        prepare_imgui(&mut imgui);
1305        let plot_a = PlotContext::create(&imgui);
1306        let raw_a = plot_a.raw;
1307        let plot_b = PlotContext::create(&imgui);
1308        let raw_b = plot_b.raw;
1309
1310        {
1311            let ui = imgui.frame();
1312            let plot_ui = plot_a.get_plot_ui(&ui);
1313            let token = plot_ui.begin_plot("token").expect("failed to begin plot");
1314
1315            unsafe { sys::ImPlot_SetCurrentContext(raw_b) };
1316            drop(token);
1317
1318            assert_eq!(unsafe { sys::ImPlot_GetCurrentContext() }, raw_a);
1319        }
1320        let _ = imgui.render();
1321
1322        drop(plot_b);
1323        drop(plot_a);
1324    }
1325
1326    #[test]
1327    fn current_context_wrapper_is_non_owning() {
1328        let _guard = test_guard();
1329        let imgui = Context::create();
1330        let plot = PlotContext::create(&imgui);
1331        let raw = plot.raw;
1332
1333        let borrowed = unsafe { PlotContext::current() }.expect("expected current ImPlot context");
1334        drop(borrowed);
1335
1336        assert_eq!(unsafe { sys::ImPlot_GetCurrentContext() }, raw);
1337        plot.set_as_current();
1338
1339        drop(plot);
1340    }
1341
1342    #[test]
1343    #[should_panic(expected = "PlotUi::set_next_x_axis_limits() min must be finite")]
1344    fn set_next_axis_limits_rejects_non_finite_values_before_ffi() {
1345        let _guard = test_guard();
1346        let mut imgui = Context::create();
1347        prepare_imgui(&mut imgui);
1348        let plot = PlotContext::create(&imgui);
1349
1350        {
1351            let ui = imgui.frame();
1352            let plot_ui = plot.get_plot_ui(&ui);
1353            plot_ui.set_next_x_axis_limits(XAxis::X1, f64::NAN, 1.0, PlotCond::Once);
1354        }
1355    }
1356
1357    #[test]
1358    #[should_panic(expected = "PlotUi::setup_axis_zoom_constraints() min must be positive")]
1359    fn axis_zoom_constraints_reject_non_positive_min_before_ffi() {
1360        let _guard = test_guard();
1361        let mut imgui = Context::create();
1362        prepare_imgui(&mut imgui);
1363        let plot = PlotContext::create(&imgui);
1364
1365        {
1366            let ui = imgui.frame();
1367            let plot_ui = plot.get_plot_ui(&ui);
1368            let token = plot_ui
1369                .begin_plot("constraints")
1370                .expect("failed to begin plot");
1371            plot_ui.setup_axis_zoom_constraints(Axis::Y1, 0.0, 10.0);
1372            token.end();
1373        }
1374    }
1375
1376    #[test]
1377    fn typed_axis_apis_accept_valid_axes() {
1378        let _guard = test_guard();
1379        let mut imgui = Context::create();
1380        prepare_imgui(&mut imgui);
1381        let plot = PlotContext::create(&imgui);
1382
1383        {
1384            let ui = imgui.frame();
1385            let plot_ui = plot.get_plot_ui(&ui);
1386            plot_ui.set_next_axis_to_fit(Axis::X1);
1387
1388            let token = plot_ui
1389                .begin_plot("typed-axis")
1390                .expect("failed to begin plot");
1391            plot_ui.setup_x_axis(XAxis::X1, None, crate::AxisFlags::NONE);
1392            plot_ui.setup_y_axis(YAxis::Y1, None, crate::AxisFlags::NONE);
1393            let mut min = 0.0;
1394            let mut max = 1.0;
1395            plot_ui.setup_axis_links(Axis::Y1, Some(&mut min), Some(&mut max));
1396            plot_ui.setup_axis_limits_constraints(Axis::Y1, -10.0, 10.0);
1397            plot_ui.setup_axis_zoom_constraints(Axis::Y1, 0.1, 20.0);
1398            token.end();
1399        }
1400
1401        let _ = imgui.render();
1402        drop(plot);
1403    }
1404}