Skip to main content

dear_implot/context/
callbacks.rs

1use super::ui::PlotUi;
2use crate::{XAxis, YAxis, sys};
3use std::{cell::RefCell, rc::Rc};
4
5impl<'ui> PlotUi<'ui> {
6    // -------- Formatter (closure) --------
7    /// Setup tick label formatter using a Rust closure.
8    ///
9    /// The closure is kept alive until the current plot ends.
10    pub fn setup_x_axis_format_closure<F>(&self, axis: XAxis, f: F) -> AxisFormatterToken
11    where
12        F: Fn(f64) -> String + Send + Sync + 'static,
13    {
14        let _guard = self.bind();
15        AxisFormatterToken::new(axis as sys::ImAxis, f)
16    }
17
18    /// Setup tick label formatter using a Rust closure.
19    ///
20    /// The closure is kept alive until the current plot ends.
21    pub fn setup_y_axis_format_closure<F>(&self, axis: YAxis, f: F) -> AxisFormatterToken
22    where
23        F: Fn(f64) -> String + Send + Sync + 'static,
24    {
25        let _guard = self.bind();
26        AxisFormatterToken::new(axis as sys::ImAxis, f)
27    }
28
29    // -------- Transform (closure) --------
30    /// Setup custom axis transform using Rust closures (forward/inverse).
31    ///
32    /// The closures are kept alive until the current plot ends.
33    pub fn setup_x_axis_transform_closure<FW, INV>(
34        &self,
35        axis: XAxis,
36        forward: FW,
37        inverse: INV,
38    ) -> AxisTransformToken
39    where
40        FW: Fn(f64) -> f64 + Send + Sync + 'static,
41        INV: Fn(f64) -> f64 + Send + Sync + 'static,
42    {
43        let _guard = self.bind();
44        AxisTransformToken::new(axis as sys::ImAxis, forward, inverse)
45    }
46
47    /// Setup custom axis transform for Y axis using closures
48    pub fn setup_y_axis_transform_closure<FW, INV>(
49        &self,
50        axis: YAxis,
51        forward: FW,
52        inverse: INV,
53    ) -> AxisTransformToken
54    where
55        FW: Fn(f64) -> f64 + Send + Sync + 'static,
56        INV: Fn(f64) -> f64 + Send + Sync + 'static,
57    {
58        let _guard = self.bind();
59        AxisTransformToken::new(axis as sys::ImAxis, forward, inverse)
60    }
61}
62
63// Plot-scope callback storage -------------------------------------------------
64//
65// ImPlot's axis formatter/transform APIs take function pointers + `user_data`
66// pointers, and may call them at any point until the current plot ends.
67//
68// Returning a standalone token that owns the closure is unsound: safe Rust code
69// could drop the token early, leaving ImPlot with a dangling `user_data` pointer.
70//
71// To keep the safe API sound without forcing users to manually retain tokens,
72// we store callback holders in thread-local, plot-scoped storage that is
73// created when a plot begins and destroyed when the plot ends.
74
75#[derive(Default)]
76struct PlotScopeStorage {
77    formatters: Vec<Box<FormatterHolder>>,
78    transforms: Vec<Box<TransformHolder>>,
79}
80
81thread_local! {
82    static PLOT_SCOPE_STACK: RefCell<Vec<PlotScopeStorage>> = const { RefCell::new(Vec::new()) };
83}
84
85fn with_plot_scope_storage<T>(f: impl FnOnce(&mut PlotScopeStorage) -> T) -> Option<T> {
86    PLOT_SCOPE_STACK.with(|stack| {
87        let mut stack = stack.borrow_mut();
88        stack.last_mut().map(f)
89    })
90}
91
92pub(crate) struct PlotScopeGuard {
93    _not_send_or_sync: std::marker::PhantomData<Rc<()>>,
94}
95
96impl PlotScopeGuard {
97    pub(crate) fn new() -> Self {
98        PLOT_SCOPE_STACK.with(|stack| stack.borrow_mut().push(PlotScopeStorage::default()));
99        Self {
100            _not_send_or_sync: std::marker::PhantomData,
101        }
102    }
103}
104
105impl Drop for PlotScopeGuard {
106    fn drop(&mut self) {
107        PLOT_SCOPE_STACK.with(|stack| {
108            let popped = stack.borrow_mut().pop();
109            debug_assert!(popped.is_some(), "dear-implot: plot scope stack underflow");
110        });
111    }
112}
113
114// =================== Formatter bridge ===================
115
116struct FormatterHolder {
117    func: Box<dyn Fn(f64) -> String + Send + Sync + 'static>,
118}
119
120#[must_use]
121pub struct AxisFormatterToken {
122    _private: (),
123    _not_send_or_sync: std::marker::PhantomData<Rc<()>>,
124}
125
126impl AxisFormatterToken {
127    fn new<F>(axis: sys::ImAxis, f: F) -> Self
128    where
129        F: Fn(f64) -> String + Send + Sync + 'static,
130    {
131        let configured = with_plot_scope_storage(|storage| {
132            let holder = Box::new(FormatterHolder { func: Box::new(f) });
133            let user = &*holder as *const FormatterHolder as *mut std::os::raw::c_void;
134            storage.formatters.push(holder);
135            unsafe {
136                sys::ImPlot_SetupAxisFormat_PlotFormatter(
137                    axis as sys::ImAxis,
138                    Some(formatter_thunk),
139                    user,
140                )
141            }
142        })
143        .is_some();
144
145        debug_assert!(
146            configured,
147            "dear-implot: axis formatter closure must be set within an active plot"
148        );
149
150        Self {
151            _private: (),
152            _not_send_or_sync: std::marker::PhantomData,
153        }
154    }
155}
156
157impl Drop for AxisFormatterToken {
158    fn drop(&mut self) {
159        // The actual callback lifetime is managed by PlotScopeGuard.
160    }
161}
162
163unsafe extern "C" fn formatter_thunk(
164    value: f64,
165    buff: *mut std::os::raw::c_char,
166    size: std::os::raw::c_int,
167    user_data: *mut std::os::raw::c_void,
168) -> std::os::raw::c_int {
169    if user_data.is_null() || buff.is_null() || size <= 0 {
170        return 0;
171    }
172    // Safety: ImPlot passes back the same pointer we provided in `AxisFormatterToken::new`.
173    let holder = unsafe { &*(user_data as *const FormatterHolder) };
174    let s = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (holder.func)(value))) {
175        Ok(v) => v,
176        Err(_) => {
177            eprintln!("dear-implot: panic in axis formatter callback");
178            std::process::abort();
179        }
180    };
181    let bytes = s.as_bytes();
182    let max = (size - 1).max(0) as usize;
183    let n = bytes.len().min(max);
184
185    // Safety: `buff` is assumed to point to a valid buffer of at least `size`
186    // bytes, with space for a terminating null. This matches ImPlot's
187    // formatter contract.
188    unsafe {
189        std::ptr::copy_nonoverlapping(bytes.as_ptr(), buff as *mut u8, n);
190        *buff.add(n) = 0;
191    }
192    n as std::os::raw::c_int
193}
194
195// =================== Transform bridge ===================
196
197struct TransformHolder {
198    forward: Box<dyn Fn(f64) -> f64 + Send + Sync + 'static>,
199    inverse: Box<dyn Fn(f64) -> f64 + Send + Sync + 'static>,
200}
201
202#[must_use]
203pub struct AxisTransformToken {
204    _private: (),
205    _not_send_or_sync: std::marker::PhantomData<Rc<()>>,
206}
207
208impl AxisTransformToken {
209    fn new<FW, INV>(axis: sys::ImAxis, forward: FW, inverse: INV) -> Self
210    where
211        FW: Fn(f64) -> f64 + Send + Sync + 'static,
212        INV: Fn(f64) -> f64 + Send + Sync + 'static,
213    {
214        let configured = with_plot_scope_storage(|storage| {
215            let holder = Box::new(TransformHolder {
216                forward: Box::new(forward),
217                inverse: Box::new(inverse),
218            });
219            let user = &*holder as *const TransformHolder as *mut std::os::raw::c_void;
220            storage.transforms.push(holder);
221            unsafe {
222                sys::ImPlot_SetupAxisScale_PlotTransform(
223                    axis as sys::ImAxis,
224                    Some(transform_forward_thunk),
225                    Some(transform_inverse_thunk),
226                    user,
227                )
228            }
229        })
230        .is_some();
231
232        debug_assert!(
233            configured,
234            "dear-implot: axis transform closure must be set within an active plot"
235        );
236
237        Self {
238            _private: (),
239            _not_send_or_sync: std::marker::PhantomData,
240        }
241    }
242}
243
244impl Drop for AxisTransformToken {
245    fn drop(&mut self) {
246        // The actual callback lifetime is managed by PlotScopeGuard.
247    }
248}
249
250unsafe extern "C" fn transform_forward_thunk(
251    value: f64,
252    user_data: *mut std::os::raw::c_void,
253) -> f64 {
254    if user_data.is_null() {
255        return value;
256    }
257    let holder = unsafe { &*(user_data as *const TransformHolder) };
258    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (holder.forward)(value))) {
259        Ok(v) => v,
260        Err(_) => {
261            eprintln!("dear-implot: panic in axis transform (forward) callback");
262            std::process::abort();
263        }
264    }
265}
266
267unsafe extern "C" fn transform_inverse_thunk(
268    value: f64,
269    user_data: *mut std::os::raw::c_void,
270) -> f64 {
271    if user_data.is_null() {
272        return value;
273    }
274    let holder = unsafe { &*(user_data as *const TransformHolder) };
275    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (holder.inverse)(value))) {
276        Ok(v) => v,
277        Err(_) => {
278            eprintln!("dear-implot: panic in axis transform (inverse) callback");
279            std::process::abort();
280        }
281    }
282}