Skip to main content

dear_implot/
utils.rs

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