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