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