Skip to main content

dear_implot3d/
layout.rs

1use std::cell::RefCell;
2
3use crate::{imgui_sys, sys};
4
5pub(crate) fn len_i32(len: usize) -> Option<i32> {
6    i32::try_from(len).ok()
7}
8
9pub(crate) fn axis_tick_count_to_i32(caller: &str, count: usize) -> i32 {
10    assert!(count > 0, "{caller} n_ticks must be positive");
11    i32::try_from(count)
12        .unwrap_or_else(|_| panic!("{caller} n_ticks exceeded ImPlot3D's i32 range"))
13}
14
15pub(crate) fn surface_count_to_i32(count: usize) -> Option<i32> {
16    let count = i32::try_from(count).ok()?;
17    (count > 0).then_some(count)
18}
19
20pub(crate) const IMPLOT3D_AUTO: i32 = -1;
21
22thread_local! {
23    static NEXT_PLOT3D_SPEC: RefCell<Option<sys::ImPlot3DSpec_c>> = RefCell::new(None);
24}
25
26/// Sample-index offset used by ImPlot3D item data access.
27///
28/// ImPlot3D intentionally allows negative and out-of-range offsets for circular
29/// buffers, so this is a signed sample offset rather than a Rust slice index.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
31pub struct Plot3DDataOffset(i32);
32
33impl Plot3DDataOffset {
34    /// No data offset.
35    pub const ZERO: Self = Self(0);
36
37    /// Create a sample-index offset.
38    #[inline]
39    pub const fn samples(offset: i32) -> Self {
40        Self(offset)
41    }
42
43    #[inline]
44    pub(crate) const fn raw(self) -> i32 {
45        self.0
46    }
47}
48
49/// Byte stride used by ImPlot3D item data access.
50///
51/// Use [`Plot3DDataStride::AUTO`] for contiguous data of the plotted value type,
52/// or [`Plot3DDataStride::bytes`] for interleaved/custom layouts.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct Plot3DDataStride(i32);
55
56impl Plot3DDataStride {
57    /// Let ImPlot3D use `sizeof(T)` for the plotted value type.
58    pub const AUTO: Self = Self(IMPLOT3D_AUTO);
59
60    /// Create a byte stride.
61    ///
62    /// Panics if `bytes` is zero or exceeds ImPlot3D's `int` range.
63    #[inline]
64    pub fn bytes(bytes: usize) -> Self {
65        assert!(
66            bytes > 0,
67            "Plot3DDataStride::bytes() requires a non-zero stride"
68        );
69        let bytes = i32::try_from(bytes)
70            .expect("Plot3DDataStride::bytes() stride exceeded ImPlot3D's int range");
71        Self(bytes)
72    }
73
74    /// Create the contiguous byte stride for `T`.
75    #[inline]
76    pub fn for_type<T>() -> Self {
77        Self::bytes(std::mem::size_of::<T>())
78    }
79
80    #[inline]
81    pub(crate) const fn raw(self) -> i32 {
82        self.0
83    }
84}
85
86impl Default for Plot3DDataStride {
87    fn default() -> Self {
88        Self::AUTO
89    }
90}
91
92/// Data layout used by ImPlot3D item builders.
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
94pub struct Plot3DDataLayout {
95    offset: Plot3DDataOffset,
96    stride: Plot3DDataStride,
97}
98
99impl Plot3DDataLayout {
100    /// Contiguous data starting at sample offset zero.
101    pub const DEFAULT: Self = Self {
102        offset: Plot3DDataOffset::ZERO,
103        stride: Plot3DDataStride::AUTO,
104    };
105
106    /// Create a data layout from a sample offset and byte stride.
107    #[inline]
108    pub const fn new(offset: Plot3DDataOffset, stride: Plot3DDataStride) -> Self {
109        Self { offset, stride }
110    }
111
112    /// Create a data layout with a different sample offset.
113    #[inline]
114    pub const fn with_offset(mut self, offset: Plot3DDataOffset) -> Self {
115        self.offset = offset;
116        self
117    }
118
119    /// Create a data layout with a different byte stride.
120    #[inline]
121    pub const fn with_stride(mut self, stride: Plot3DDataStride) -> Self {
122        self.stride = stride;
123        self
124    }
125
126    #[inline]
127    pub(crate) const fn raw_offset(self) -> i32 {
128        self.offset.raw()
129    }
130
131    #[inline]
132    pub(crate) const fn raw_stride(self) -> i32 {
133        self.stride.raw()
134    }
135}
136
137pub(crate) fn update_next_plot3d_spec(f: impl FnOnce(&mut sys::ImPlot3DSpec_c)) {
138    NEXT_PLOT3D_SPEC.with(|cell| {
139        let mut guard = cell.borrow_mut();
140        let mut spec = guard.take().unwrap_or_else(default_plot3d_spec);
141        f(&mut spec);
142        *guard = Some(spec);
143    })
144}
145
146pub(crate) fn take_next_plot3d_spec() -> Option<sys::ImPlot3DSpec_c> {
147    NEXT_PLOT3D_SPEC.with(|cell| cell.borrow_mut().take())
148}
149
150pub(crate) fn set_next_plot3d_spec(spec: Option<sys::ImPlot3DSpec_c>) {
151    NEXT_PLOT3D_SPEC.with(|cell| {
152        *cell.borrow_mut() = spec;
153    })
154}
155
156pub(crate) fn default_plot3d_spec() -> sys::ImPlot3DSpec_c {
157    let auto_col = sys::ImVec4_c {
158        x: 0.0,
159        y: 0.0,
160        z: 0.0,
161        w: -1.0,
162    };
163
164    sys::ImPlot3DSpec_c {
165        LineColor: auto_col,
166        LineColors: std::ptr::null_mut(),
167        LineWeight: 1.0,
168        FillColor: auto_col,
169        FillColors: std::ptr::null_mut(),
170        FillAlpha: -1.0,
171        Marker: sys::ImPlot3DMarker_Auto as _,
172        MarkerSize: -1.0,
173        MarkerSizes: std::ptr::null_mut(),
174        MarkerLineColor: auto_col,
175        MarkerLineColors: std::ptr::null_mut(),
176        MarkerFillColor: auto_col,
177        MarkerFillColors: std::ptr::null_mut(),
178        Offset: 0,
179        Stride: IMPLOT3D_AUTO,
180        Flags: sys::ImPlot3DItemFlags_None as _,
181    }
182}
183
184pub(crate) fn plot3d_spec_from(flags: u32, layout: Plot3DDataLayout) -> sys::ImPlot3DSpec_c {
185    let mut spec = take_next_plot3d_spec().unwrap_or_else(default_plot3d_spec);
186    spec.Flags = ((spec.Flags as u32) | flags) as sys::ImPlot3DItemFlags;
187    spec.Offset = layout.raw_offset();
188    spec.Stride = layout.raw_stride();
189    spec
190}
191
192pub(crate) trait ImVec2Ctor {
193    fn from_xy(x: f32, y: f32) -> Self;
194}
195
196impl ImVec2Ctor for sys::ImVec2_c {
197    fn from_xy(x: f32, y: f32) -> Self {
198        Self { x, y }
199    }
200}
201
202impl ImVec2Ctor for imgui_sys::ImVec2_c {
203    fn from_xy(x: f32, y: f32) -> Self {
204        Self { x, y }
205    }
206}
207
208#[inline]
209pub(crate) fn imvec2<T: ImVec2Ctor>(x: f32, y: f32) -> T {
210    T::from_xy(x, y)
211}
212
213pub(crate) trait ImVec4Ctor {
214    fn from_xyzw(x: f32, y: f32, z: f32, w: f32) -> Self;
215}
216
217impl ImVec4Ctor for sys::ImVec4_c {
218    fn from_xyzw(x: f32, y: f32, z: f32, w: f32) -> Self {
219        Self { x, y, z, w }
220    }
221}
222
223impl ImVec4Ctor for imgui_sys::ImVec4_c {
224    fn from_xyzw(x: f32, y: f32, z: f32, w: f32) -> Self {
225        Self { x, y, z, w }
226    }
227}
228
229#[inline]
230pub(crate) fn imvec4<T: ImVec4Ctor>(x: f32, y: f32, z: f32, w: f32) -> T {
231    T::from_xyzw(x, y, z, w)
232}
233
234#[cfg(test)]
235mod tests {
236    use super::{
237        Plot3DDataLayout, Plot3DDataOffset, Plot3DDataStride, axis_tick_count_to_i32,
238        plot3d_spec_from, surface_count_to_i32,
239    };
240
241    #[test]
242    fn data_layout_allows_signed_sample_offsets() {
243        let layout = Plot3DDataLayout::DEFAULT.with_offset(Plot3DDataOffset::samples(-8));
244        let spec = plot3d_spec_from(0, layout);
245        assert_eq!(spec.Offset, -8);
246        assert_eq!(spec.Stride, super::IMPLOT3D_AUTO);
247    }
248
249    #[test]
250    fn data_stride_bytes_are_positive() {
251        let stride = Plot3DDataStride::bytes(16);
252        let layout = Plot3DDataLayout::DEFAULT.with_stride(stride);
253        let spec = plot3d_spec_from(0, layout);
254        assert_eq!(spec.Stride, 16);
255    }
256
257    #[test]
258    #[should_panic(expected = "requires a non-zero stride")]
259    fn zero_data_stride_panics_before_ffi() {
260        let _ = Plot3DDataStride::bytes(0);
261    }
262
263    #[test]
264    fn surface_count_checks_implot3d_i32_range() {
265        assert_eq!(surface_count_to_i32(1), Some(1));
266        assert_eq!(surface_count_to_i32(0), None);
267        assert_eq!(surface_count_to_i32(i32::MAX as usize), Some(i32::MAX));
268        assert_eq!(surface_count_to_i32(i32::MAX as usize + 1), None);
269    }
270
271    #[test]
272    fn axis_ticks_range_count_is_checked_before_ffi() {
273        assert_eq!(axis_tick_count_to_i32("test", 1), 1);
274        assert_eq!(axis_tick_count_to_i32("test", i32::MAX as usize), i32::MAX);
275
276        assert!(
277            std::panic::catch_unwind(|| axis_tick_count_to_i32("test", 0)).is_err(),
278            "zero tick counts must not cross the safe API boundary"
279        );
280        assert!(
281            std::panic::catch_unwind(|| {
282                axis_tick_count_to_i32("test", i32::MAX as usize + 1);
283            })
284            .is_err(),
285            "oversized tick counts must not cross the safe API boundary"
286        );
287    }
288}