Skip to main content

dear_implot/
utils.rs

1// Utility functions for ImPlot
2
3use crate::{Axis, PlotUi, XAxis, YAxis, sys};
4use dear_imgui_rs::with_scratch_txt;
5use std::{borrow::Cow, fmt};
6
7fn literal_printf_format(text: &str) -> Cow<'_, str> {
8    if text.contains('%') {
9        Cow::Owned(text.replace('%', "%%"))
10    } else {
11        Cow::Borrowed(text)
12    }
13}
14
15fn assert_finite_f64(caller: &str, name: &str, value: f64) {
16    assert!(value.is_finite(), "{caller} {name} must be finite");
17}
18
19fn assert_finite_vec2(caller: &str, name: &str, value: [f32; 2]) {
20    assert!(
21        value[0].is_finite() && value[1].is_finite(),
22        "{caller} {name} must be finite"
23    );
24}
25
26fn assert_finite_color(caller: &str, name: &str, value: [f32; 4]) {
27    assert!(
28        value.iter().all(|component| component.is_finite()),
29        "{caller} {name} must be finite"
30    );
31}
32
33fn assert_finite_point(caller: &str, name: &str, value: sys::ImPlotPoint) {
34    assert!(
35        value.x.is_finite() && value.y.is_finite(),
36        "{caller} {name} must be finite"
37    );
38}
39
40impl PlotUi<'_> {
41    /// Check if any subplots area is hovered.
42    pub fn is_subplots_hovered(&self) -> bool {
43        self.with_bound_context(|| unsafe { sys::ImPlot_IsSubplotsHovered() })
44    }
45
46    /// Check if a legend entry is hovered.
47    pub fn is_legend_entry_hovered(&self, label: &str) -> bool {
48        let label = if label.contains('\0') { "" } else { label };
49        self.with_bound_context(|| {
50            with_scratch_txt(label, |ptr| unsafe {
51                sys::ImPlot_IsLegendEntryHovered(ptr)
52            })
53        })
54    }
55
56    /// Get the mouse position in plot coordinates.
57    pub fn plot_mouse_position(
58        &self,
59        y_axis_choice: Option<crate::YAxisChoice>,
60    ) -> sys::ImPlotPoint {
61        let x_axis = 0; // ImAxis_X1
62        let y_axis = match y_axis_choice {
63            Some(crate::YAxisChoice::First) => 3,  // ImAxis_Y1
64            Some(crate::YAxisChoice::Second) => 4, // ImAxis_Y2
65            Some(crate::YAxisChoice::Third) => 5,  // ImAxis_Y3
66            None => 3,                             // Default to Y1
67        };
68        self.with_bound_context(|| unsafe {
69            sys::ImPlot_GetPlotMousePos(x_axis as sys::ImAxis, y_axis as sys::ImAxis)
70        })
71    }
72
73    /// Get the mouse position in plot coordinates for specific axes.
74    pub fn plot_mouse_position_axes(&self, x_axis: XAxis, y_axis: YAxis) -> sys::ImPlotPoint {
75        self.with_bound_context(|| unsafe {
76            sys::ImPlot_GetPlotMousePos(x_axis as sys::ImAxis, y_axis as sys::ImAxis)
77        })
78    }
79
80    /// Convert pixels to plot coordinates.
81    pub fn pixels_to_plot(
82        &self,
83        pixel_position: [f32; 2],
84        y_axis_choice: Option<crate::YAxisChoice>,
85    ) -> sys::ImPlotPoint {
86        assert_finite_vec2("PlotUi::pixels_to_plot()", "pixel_position", pixel_position);
87        // Map absolute pixel coordinates to plot coordinates using current plot's axes.
88        let y_index = match y_axis_choice {
89            Some(crate::YAxisChoice::First) => 0,
90            Some(crate::YAxisChoice::Second) => 1,
91            Some(crate::YAxisChoice::Third) => 2,
92            None => 0,
93        };
94        self.with_bound_context(|| unsafe {
95            let plot = sys::ImPlot_GetCurrentPlot();
96            if plot.is_null() {
97                return sys::ImPlotPoint { x: 0.0, y: 0.0 };
98            }
99            let x_axis_ptr = sys::ImPlotPlot_XAxis_Nil(plot, 0);
100            let y_axis_ptr = sys::ImPlotPlot_YAxis_Nil(plot, y_index);
101            let x = sys::ImPlotAxis_PixelsToPlot(x_axis_ptr, pixel_position[0]);
102            let y = sys::ImPlotAxis_PixelsToPlot(y_axis_ptr, pixel_position[1]);
103            sys::ImPlotPoint { x, y }
104        })
105    }
106
107    /// Convert pixels to plot coordinates for specific axes.
108    pub fn pixels_to_plot_axes(
109        &self,
110        pixel_position: [f32; 2],
111        x_axis: XAxis,
112        y_axis: YAxis,
113    ) -> sys::ImPlotPoint {
114        assert_finite_vec2(
115            "PlotUi::pixels_to_plot_axes()",
116            "pixel_position",
117            pixel_position,
118        );
119        self.with_bound_context(|| unsafe {
120            let plot = sys::ImPlot_GetCurrentPlot();
121            if plot.is_null() {
122                return sys::ImPlotPoint { x: 0.0, y: 0.0 };
123            }
124            let x_axis_ptr = sys::ImPlotPlot_XAxis_Nil(plot, x_axis as i32);
125            let y_axis_ptr = sys::ImPlotPlot_YAxis_Nil(plot, y_axis.to_index());
126            let x = sys::ImPlotAxis_PixelsToPlot(x_axis_ptr, pixel_position[0]);
127            let y = sys::ImPlotAxis_PixelsToPlot(y_axis_ptr, pixel_position[1]);
128            sys::ImPlotPoint { x, y }
129        })
130    }
131
132    /// Convert plot coordinates to pixels.
133    pub fn plot_to_pixels(
134        &self,
135        plot_position: sys::ImPlotPoint,
136        y_axis_choice: Option<crate::YAxisChoice>,
137    ) -> [f32; 2] {
138        assert_finite_point("PlotUi::plot_to_pixels()", "plot_position", plot_position);
139        let y_index = match y_axis_choice {
140            Some(crate::YAxisChoice::First) => 0,
141            Some(crate::YAxisChoice::Second) => 1,
142            Some(crate::YAxisChoice::Third) => 2,
143            None => 0,
144        };
145        self.with_bound_context(|| unsafe {
146            let plot = sys::ImPlot_GetCurrentPlot();
147            if plot.is_null() {
148                return [0.0, 0.0];
149            }
150            let x_axis_ptr = sys::ImPlotPlot_XAxis_Nil(plot, 0);
151            let y_axis_ptr = sys::ImPlotPlot_YAxis_Nil(plot, y_index);
152            let px = sys::ImPlotAxis_PlotToPixels(x_axis_ptr, plot_position.x);
153            let py = sys::ImPlotAxis_PlotToPixels(y_axis_ptr, plot_position.y);
154            [px, py]
155        })
156    }
157
158    /// Convert plot coordinates to pixels for specific axes.
159    pub fn plot_to_pixels_axes(
160        &self,
161        plot_position: sys::ImPlotPoint,
162        x_axis: XAxis,
163        y_axis: YAxis,
164    ) -> [f32; 2] {
165        assert_finite_point(
166            "PlotUi::plot_to_pixels_axes()",
167            "plot_position",
168            plot_position,
169        );
170        self.with_bound_context(|| unsafe {
171            let plot = sys::ImPlot_GetCurrentPlot();
172            if plot.is_null() {
173                return [0.0, 0.0];
174            }
175            let x_axis_ptr = sys::ImPlotPlot_XAxis_Nil(plot, x_axis as i32);
176            let y_axis_ptr = sys::ImPlotPlot_YAxis_Nil(plot, y_axis.to_index());
177            let px = sys::ImPlotAxis_PlotToPixels(x_axis_ptr, plot_position.x);
178            let py = sys::ImPlotAxis_PlotToPixels(y_axis_ptr, plot_position.y);
179            [px, py]
180        })
181    }
182
183    /// Get the current plot limits.
184    pub fn plot_limits(
185        &self,
186        _x_axis_choice: Option<crate::YAxisChoice>,
187        y_axis_choice: Option<crate::YAxisChoice>,
188    ) -> sys::ImPlotRect {
189        let x_axis = 0; // ImAxis_X1
190        let y_axis = match y_axis_choice {
191            Some(crate::YAxisChoice::First) => 3,  // ImAxis_Y1
192            Some(crate::YAxisChoice::Second) => 4, // ImAxis_Y2
193            Some(crate::YAxisChoice::Third) => 5,  // ImAxis_Y3
194            None => 3,                             // Default to Y1
195        };
196        self.with_bound_context(|| unsafe { sys::ImPlot_GetPlotLimits(x_axis, y_axis) })
197    }
198
199    /// Whether a plot has an active selection region.
200    pub fn is_plot_selected(&self) -> bool {
201        self.with_bound_context(|| unsafe { sys::ImPlot_IsPlotSelected() })
202    }
203
204    /// Get the current plot selection rectangle for specific axes.
205    pub fn plot_selection_axes(&self, x_axis: XAxis, y_axis: YAxis) -> Option<sys::ImPlotRect> {
206        if !self.is_plot_selected() {
207            return None;
208        }
209        self.with_bound_context(|| {
210            let rect = unsafe { sys::ImPlot_GetPlotSelection(x_axis as i32, y_axis as i32) };
211            Some(rect)
212        })
213    }
214
215    /// Draw a simple round annotation marker at (x,y).
216    pub fn annotation_point(
217        &self,
218        x: f64,
219        y: f64,
220        color: [f32; 4],
221        pixel_offset: [f32; 2],
222        clamp: bool,
223        round: bool,
224    ) {
225        assert_finite_f64("PlotUi::annotation_point()", "x", x);
226        assert_finite_f64("PlotUi::annotation_point()", "y", y);
227        assert_finite_color("PlotUi::annotation_point()", "color", color);
228        assert_finite_vec2("PlotUi::annotation_point()", "pixel_offset", pixel_offset);
229        let col = sys::ImVec4_c {
230            x: color[0],
231            y: color[1],
232            z: color[2],
233            w: color[3],
234        };
235        let off = sys::ImVec2_c {
236            x: pixel_offset[0],
237            y: pixel_offset[1],
238        };
239        self.with_bound_context(|| unsafe {
240            sys::ImPlot_Annotation_Bool(x, y, col, off, clamp, round)
241        })
242    }
243
244    /// Draw a literal text annotation at (x,y).
245    ///
246    /// The underlying non-variadic C entrypoint still accepts a `printf` format
247    /// string, so percent signs are escaped before crossing the FFI boundary.
248    pub fn annotation_text(
249        &self,
250        x: f64,
251        y: f64,
252        color: [f32; 4],
253        pixel_offset: [f32; 2],
254        clamp: bool,
255        text: &str,
256    ) {
257        assert_finite_f64("PlotUi::annotation_text()", "x", x);
258        assert_finite_f64("PlotUi::annotation_text()", "y", y);
259        assert_finite_color("PlotUi::annotation_text()", "color", color);
260        assert_finite_vec2("PlotUi::annotation_text()", "pixel_offset", pixel_offset);
261        let col = sys::ImVec4_c {
262            x: color[0],
263            y: color[1],
264            z: color[2],
265            w: color[3],
266        };
267        let off = sys::ImVec2_c {
268            x: pixel_offset[0],
269            y: pixel_offset[1],
270        };
271        assert!(!text.contains('\0'), "text contained NUL");
272        let text = literal_printf_format(text);
273        self.with_bound_context(|| {
274            with_scratch_txt(text.as_ref(), |ptr| unsafe {
275                sys::ImPlot_Annotation_Str0(x, y, col, off, clamp, ptr)
276            })
277        })
278    }
279
280    /// Tag the X axis at position x with a tick-like mark.
281    pub fn tag_x(&self, x: f64, color: [f32; 4], round: bool) {
282        assert_finite_f64("PlotUi::tag_x()", "x", x);
283        assert_finite_color("PlotUi::tag_x()", "color", color);
284        let col = sys::ImVec4_c {
285            x: color[0],
286            y: color[1],
287            z: color[2],
288            w: color[3],
289        };
290        self.with_bound_context(|| unsafe { sys::ImPlot_TagX_Bool(x, col, round) })
291    }
292
293    /// Tag the X axis at position x with a literal text label.
294    pub fn tag_x_text(&self, x: f64, color: [f32; 4], text: &str) {
295        assert_finite_f64("PlotUi::tag_x_text()", "x", x);
296        assert_finite_color("PlotUi::tag_x_text()", "color", color);
297        let col = sys::ImVec4_c {
298            x: color[0],
299            y: color[1],
300            z: color[2],
301            w: color[3],
302        };
303        assert!(!text.contains('\0'), "text contained NUL");
304        let text = literal_printf_format(text);
305        self.with_bound_context(|| {
306            with_scratch_txt(text.as_ref(), |ptr| unsafe {
307                sys::ImPlot_TagX_Str0(x, col, ptr)
308            })
309        })
310    }
311
312    /// Tag the Y axis at position y with a tick-like mark.
313    pub fn tag_y(&self, y: f64, color: [f32; 4], round: bool) {
314        assert_finite_f64("PlotUi::tag_y()", "y", y);
315        assert_finite_color("PlotUi::tag_y()", "color", color);
316        let col = sys::ImVec4_c {
317            x: color[0],
318            y: color[1],
319            z: color[2],
320            w: color[3],
321        };
322        self.with_bound_context(|| unsafe { sys::ImPlot_TagY_Bool(y, col, round) })
323    }
324
325    /// Tag the Y axis at position y with a literal text label.
326    pub fn tag_y_text(&self, y: f64, color: [f32; 4], text: &str) {
327        assert_finite_f64("PlotUi::tag_y_text()", "y", y);
328        assert_finite_color("PlotUi::tag_y_text()", "color", color);
329        let col = sys::ImVec4_c {
330            x: color[0],
331            y: color[1],
332            z: color[2],
333            w: color[3],
334        };
335        assert!(!text.contains('\0'), "text contained NUL");
336        let text = literal_printf_format(text);
337        self.with_bound_context(|| {
338            with_scratch_txt(text.as_ref(), |ptr| unsafe {
339                sys::ImPlot_TagY_Str0(y, col, ptr)
340            })
341        })
342    }
343
344    /// Get the current plot limits for specific axes.
345    pub fn plot_limits_axes(&self, x_axis: XAxis, y_axis: YAxis) -> sys::ImPlotRect {
346        self.with_bound_context(|| unsafe {
347            sys::ImPlot_GetPlotLimits(x_axis as i32, y_axis as i32)
348        })
349    }
350
351    /// Check if an axis is hovered.
352    pub fn is_axis_hovered(&self, axis: Axis) -> bool {
353        self.with_bound_context(|| unsafe { sys::ImPlot_IsAxisHovered(axis.to_sys()) })
354    }
355
356    /// Check if a raw axis is hovered.
357    ///
358    /// # Safety
359    ///
360    /// `axis` must be a valid ImPlot `ImAxis` value for the current plot. Passing an
361    /// out-of-range value lets ImPlot index internal axis arrays out of bounds.
362    pub unsafe fn is_axis_hovered_unchecked(&self, axis: sys::ImAxis) -> bool {
363        self.with_bound_context(|| unsafe { sys::ImPlot_IsAxisHovered(axis) })
364    }
365
366    /// Check if the X axis is hovered.
367    pub fn is_plot_x_axis_hovered(&self) -> bool {
368        self.is_axis_hovered(Axis::X1)
369    }
370
371    /// Check if a specific X axis is hovered.
372    pub fn is_plot_x_axis_hovered_axis(&self, x_axis: XAxis) -> bool {
373        self.is_axis_hovered(x_axis.into())
374    }
375
376    /// Check if a Y axis is hovered.
377    pub fn is_plot_y_axis_hovered(&self, y_axis_choice: Option<crate::YAxisChoice>) -> bool {
378        let axis = match y_axis_choice {
379            Some(crate::YAxisChoice::First) | None => Axis::Y1,
380            Some(crate::YAxisChoice::Second) => Axis::Y2,
381            Some(crate::YAxisChoice::Third) => Axis::Y3,
382        };
383        self.is_axis_hovered(axis)
384    }
385
386    /// Check if a specific Y axis is hovered.
387    pub fn is_plot_y_axis_hovered_axis(&self, y_axis: YAxis) -> bool {
388        self.is_axis_hovered(y_axis.into())
389    }
390
391    /// Show the ImPlot demo window (requires sys demo symbols to be linked).
392    #[cfg(feature = "demo")]
393    pub fn show_demo_window(&self, show: &mut bool) {
394        self.with_bound_context(|| unsafe { sys::ImPlot_ShowDemoWindow(show) })
395    }
396
397    /// Stub when demo feature is disabled.
398    #[cfg(not(feature = "demo"))]
399    pub fn show_demo_window(&self, _show: &mut bool) {}
400
401    /// Show the built-in user guide for ImPlot.
402    pub fn show_user_guide(&self) {
403        self.with_bound_context(|| unsafe { sys::ImPlot_ShowUserGuide() })
404    }
405
406    /// Show the metrics window.
407    pub fn show_metrics_window(&self, open: &mut bool) {
408        self.with_bound_context(|| unsafe { sys::ImPlot_ShowMetricsWindow(open as *mut bool) })
409    }
410
411    /// Get current plot position (top-left) in pixels.
412    pub fn plot_pos(&self) -> [f32; 2] {
413        self.with_bound_context(|| {
414            let out = unsafe { sys::ImPlot_GetPlotPos() };
415            [out.x, out.y]
416        })
417    }
418
419    /// Get current plot size in pixels.
420    pub fn plot_size(&self) -> [f32; 2] {
421        self.with_bound_context(|| {
422            let out = unsafe { sys::ImPlot_GetPlotSize() };
423            [out.x, out.y]
424        })
425    }
426}
427
428/// Result of a drag interaction
429#[derive(Debug, Clone, Copy, Default)]
430pub struct DragResult {
431    /// True if the underlying value changed this frame
432    pub changed: bool,
433    /// True if it was clicked this frame
434    pub clicked: bool,
435    /// True if hovered this frame
436    pub hovered: bool,
437    /// True if held/active this frame
438    pub held: bool,
439}
440
441/// Stable identity for ImPlot drag point/line helpers.
442///
443/// This wraps the upstream `int id` parameter so safe Rust callers do not
444/// confuse tool identities with coordinates, counts, or other signed values.
445#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
446#[repr(transparent)]
447pub struct DragToolId(i32);
448
449impl DragToolId {
450    /// Create a drag tool identity from a raw signed id.
451    #[inline]
452    pub const fn new(id: i32) -> Self {
453        Self(id)
454    }
455
456    /// Return the raw `int` value expected by the ImPlot FFI.
457    #[inline]
458    pub const fn raw(self) -> i32 {
459        self.0
460    }
461}
462
463impl From<i32> for DragToolId {
464    #[inline]
465    fn from(value: i32) -> Self {
466        Self::new(value)
467    }
468}
469
470impl From<DragToolId> for i32 {
471    #[inline]
472    fn from(value: DragToolId) -> Self {
473        value.raw()
474    }
475}
476
477impl fmt::Display for DragToolId {
478    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
479        self.0.fmt(f)
480    }
481}
482
483fn color4(rgba: [f32; 4]) -> sys::ImVec4_c {
484    sys::ImVec4_c {
485        x: rgba[0],
486        y: rgba[1],
487        z: rgba[2],
488        w: rgba[3],
489    }
490}
491
492impl PlotUi<'_> {
493    /// Draggable point with result flags.
494    pub fn drag_point(
495        &self,
496        id: DragToolId,
497        x: &mut f64,
498        y: &mut f64,
499        color: [f32; 4],
500        size: f32,
501        flags: crate::DragToolFlags,
502    ) -> DragResult {
503        let mut clicked = false;
504        let mut hovered = false;
505        let mut held = false;
506        self.with_bound_context(|| {
507            let changed = unsafe {
508                sys::ImPlot_DragPoint(
509                    id.raw(),
510                    x as *mut f64,
511                    y as *mut f64,
512                    color4(color),
513                    size,
514                    flags.bits() as i32,
515                    &mut clicked as *mut bool,
516                    &mut hovered as *mut bool,
517                    &mut held as *mut bool,
518                )
519            };
520            DragResult {
521                changed,
522                clicked,
523                hovered,
524                held,
525            }
526        })
527    }
528
529    /// Draggable vertical line at x.
530    pub fn drag_line_x(
531        &self,
532        id: DragToolId,
533        x: &mut f64,
534        color: [f32; 4],
535        thickness: f32,
536        flags: crate::DragToolFlags,
537    ) -> DragResult {
538        let mut clicked = false;
539        let mut hovered = false;
540        let mut held = false;
541        self.with_bound_context(|| {
542            let changed = unsafe {
543                sys::ImPlot_DragLineX(
544                    id.raw(),
545                    x as *mut f64,
546                    color4(color),
547                    thickness,
548                    flags.bits() as i32,
549                    &mut clicked as *mut bool,
550                    &mut hovered as *mut bool,
551                    &mut held as *mut bool,
552                )
553            };
554            DragResult {
555                changed,
556                clicked,
557                hovered,
558                held,
559            }
560        })
561    }
562
563    /// Draggable horizontal line at y.
564    pub fn drag_line_y(
565        &self,
566        id: DragToolId,
567        y: &mut f64,
568        color: [f32; 4],
569        thickness: f32,
570        flags: crate::DragToolFlags,
571    ) -> DragResult {
572        let mut clicked = false;
573        let mut hovered = false;
574        let mut held = false;
575        self.with_bound_context(|| {
576            let changed = unsafe {
577                sys::ImPlot_DragLineY(
578                    id.raw(),
579                    y as *mut f64,
580                    color4(color),
581                    thickness,
582                    flags.bits() as i32,
583                    &mut clicked as *mut bool,
584                    &mut hovered as *mut bool,
585                    &mut held as *mut bool,
586                )
587            };
588            DragResult {
589                changed,
590                clicked,
591                hovered,
592                held,
593            }
594        })
595    }
596}
597
598#[cfg(test)]
599mod tests {
600    use super::{DragToolId, literal_printf_format};
601    use std::borrow::Cow;
602
603    #[test]
604    fn literal_plot_text_borrows_strings_without_percent_signs() {
605        let text = literal_printf_format("ordinary label");
606
607        assert!(matches!(text, Cow::Borrowed("ordinary label")));
608    }
609
610    #[test]
611    fn literal_plot_text_escapes_every_printf_directive() {
612        let text = literal_printf_format("100% complete: %s %n %%");
613
614        assert_eq!(text, "100%% complete: %%s %%n %%%%");
615        assert!(matches!(text, Cow::Owned(_)));
616    }
617
618    #[test]
619    fn drag_tool_id_round_trips_raw_values() {
620        let id = DragToolId::new(-7);
621        assert_eq!(id.raw(), -7);
622        assert_eq!(i32::from(id), -7);
623
624        let other = DragToolId::from(120482);
625        assert_eq!(other.raw(), 120482);
626        assert_eq!(other.to_string(), "120482");
627    }
628}