Skip to main content

dear_implot/
utils.rs

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