Skip to main content

reratui_hooks/callback/
mod.rs

1use std::fmt;
2use std::sync::Arc;
3
4use crate::effect::EffectDependencies;
5use crate::hook_context::with_hook_context;
6
7/// Universal callback wrapper inspired by Yew's Callback system.
8///
9/// This provides a type-safe way to pass function callbacks between components,
10/// similar to React's event handlers and Yew's callback system.
11///
12pub struct Callback<IN, OUT = ()> {
13    callback: Arc<dyn Fn(IN) -> OUT + Send + Sync>,
14}
15
16impl<IN, OUT> Clone for Callback<IN, OUT> {
17    fn clone(&self) -> Self {
18        Self {
19            callback: self.callback.clone(),
20        }
21    }
22}
23
24impl<IN, OUT> Callback<IN, OUT> {
25    /// Create a new callback from a function
26    pub fn new<F>(func: F) -> Self
27    where
28        F: Fn(IN) -> OUT + Send + Sync + 'static,
29    {
30        Self {
31            callback: Arc::new(func),
32        }
33    }
34
35    /// Emit the callback with the given input value
36    pub fn emit(&self, input: IN) -> OUT {
37        (self.callback)(input)
38    }
39
40    /// Create a reformed callback that transforms the input before calling this callback
41    pub fn reform<F, T>(&self, func: F) -> Callback<T, OUT>
42    where
43        F: Fn(T) -> IN + Send + Sync + 'static,
44        IN: 'static,
45        OUT: 'static,
46    {
47        let callback = self.callback.clone();
48        Callback::new(move |input: T| {
49            let transformed = func(input);
50            callback(transformed)
51        })
52    }
53
54    /// Create a reformed callback that optionally transforms the input
55    pub fn filter_reform<F, T>(&self, func: F) -> Callback<T, Option<OUT>>
56    where
57        F: Fn(T) -> Option<IN> + Send + Sync + 'static,
58        IN: 'static,
59        OUT: 'static,
60    {
61        let callback = self.callback.clone();
62        Callback::new(move |input: T| func(input).map(|v| callback(v)))
63    }
64}
65
66impl<IN> Callback<IN> {
67    /// Create a no-op callback that does nothing when called
68    /// Useful for optional callbacks or default values
69    pub fn noop() -> Self {
70        Self::new(|_| {})
71    }
72}
73
74impl<IN, OUT> Default for Callback<IN, OUT>
75where
76    OUT: Default,
77{
78    fn default() -> Self {
79        Self::new(|_| OUT::default())
80    }
81}
82
83impl<IN, OUT, F> From<F> for Callback<IN, OUT>
84where
85    F: Fn(IN) -> OUT + Send + Sync + 'static,
86{
87    fn from(func: F) -> Self {
88        Self::new(func)
89    }
90}
91
92// Additional From implementations for common patterns
93
94/// Convert from` Arc<dyn Fn>` for shared callbacks
95impl<IN, OUT> From<Arc<dyn Fn(IN) -> OUT + Send + Sync>> for Callback<IN, OUT> {
96    fn from(func: Arc<dyn Fn(IN) -> OUT + Send + Sync>) -> Self {
97        Self { callback: func }
98    }
99}
100
101/// Convert from `Option<Callback>` - None becomes a noop callback
102impl<IN> From<Option<Callback<IN>>> for Callback<IN> {
103    fn from(opt: Option<Callback<IN>>) -> Self {
104        opt.unwrap_or_else(Self::noop)
105    }
106}
107
108impl<IN, OUT> fmt::Debug for Callback<IN, OUT> {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        f.debug_struct("Callback")
111            .field("callback", &"<function>")
112            .finish()
113    }
114}
115
116impl<IN, OUT> PartialEq for Callback<IN, OUT> {
117    fn eq(&self, other: &Self) -> bool {
118        // Compare by pointer equality since we can't compare function contents
119        Arc::ptr_eq(&self.callback, &other.callback)
120    }
121}
122
123/// Trait for types that can be converted into event callbacks
124/// This allows for flexible callback prop handling in components
125pub trait IntoCallback<IN, OUT = ()> {
126    fn into_callback(self) -> Callback<IN, OUT>;
127}
128
129impl<IN, OUT, F> IntoCallback<IN, OUT> for F
130where
131    F: Fn(IN) -> OUT + Send + Sync + 'static,
132{
133    fn into_callback(self) -> Callback<IN, OUT> {
134        Callback::from(self)
135    }
136}
137
138impl<IN, OUT> IntoCallback<IN, OUT> for Callback<IN, OUT> {
139    fn into_callback(self) -> Callback<IN, OUT> {
140        self
141    }
142}
143
144/// Trait for converting callbacks into props that can be either `Option<Callback>` or Callback
145/// This handles the automatic wrapping for optional callback props in the rsx! macro
146pub trait IntoCallbackProp<T> {
147    fn into_callback_prop(self) -> T;
148}
149
150// Implementation for non-optional callback props (Callback<IN, OUT>)
151impl<F, IN, OUT> IntoCallbackProp<Callback<IN, OUT>> for F
152where
153    F: IntoCallback<IN, OUT>,
154{
155    fn into_callback_prop(self) -> Callback<IN, OUT> {
156        self.into_callback()
157    }
158}
159
160// Implementation for optional callback props (Option<Callback<IN, OUT>>)
161impl<F, IN, OUT> IntoCallbackProp<Option<Callback<IN, OUT>>> for F
162where
163    F: IntoCallback<IN, OUT>,
164{
165    fn into_callback_prop(self) -> Option<Callback<IN, OUT>> {
166        Some(self.into_callback())
167    }
168}
169
170impl<IN, OUT> Callback<IN, OUT>
171where
172    IN: 'static,
173{
174    /// Create a callback from a closure with explicit type annotation
175    /// This helps with type inference in complex scenarios
176    pub fn from_fn<F>(func: F) -> Self
177    where
178        F: Fn(IN) -> OUT + Send + Sync + 'static,
179    {
180        Self::from(func)
181    }
182
183    /// Create a callback that ignores its input and returns a constant value
184    pub fn constant(value: OUT) -> Self
185    where
186        OUT: Clone + Send + Sync + 'static,
187    {
188        Self::from(move |_| value.clone())
189    }
190
191    /// Create a callback that just prints its input (useful for debugging)
192    pub fn debug() -> Self
193    where
194        IN: std::fmt::Debug,
195        OUT: Default,
196    {
197        Self::from(|input| {
198            println!("Callback called with: {:?}", input);
199            OUT::default()
200        })
201    }
202
203    /// Chain this callback with another callback
204    /// The output of this callback becomes the input of the next
205    pub fn then<F, NextOut>(self, next: F) -> Callback<IN, NextOut>
206    where
207        F: Fn(OUT) -> NextOut + Send + Sync + 'static,
208        OUT: 'static,
209    {
210        Callback::from(move |input| {
211            let intermediate = self.emit(input);
212            next(intermediate)
213        })
214    }
215
216    /// Map the output of this callback to a different type
217    pub fn map<F, NewOut>(self, mapper: F) -> Callback<IN, NewOut>
218    where
219        F: Fn(OUT) -> NewOut + Send + Sync + 'static,
220        OUT: 'static,
221    {
222        self.then(mapper)
223    }
224
225    /// Create a callback that calls this callback only if a condition is met
226    pub fn filter<F>(self, predicate: F) -> Callback<IN, Option<OUT>>
227    where
228        F: Fn(&IN) -> bool + Send + Sync + 'static,
229        IN: Clone + 'static,
230        OUT: 'static,
231    {
232        Callback::from(move |input| {
233            if predicate(&input) {
234                Some(self.emit(input))
235            } else {
236                None
237            }
238        })
239    }
240
241    /// Create a callback that catches panics and returns a Result
242    pub fn catch_unwind(self) -> Callback<IN, Result<OUT, String>>
243    where
244        IN: 'static,
245        OUT: 'static,
246    {
247        use std::panic::{AssertUnwindSafe, catch_unwind};
248        Callback::from(move |input| {
249            catch_unwind(AssertUnwindSafe(|| self.emit(input)))
250                .map_err(|_| "Callback panicked".to_string())
251        })
252    }
253}
254
255// Additional convenience constructors
256impl<IN, OUT> Callback<IN, OUT>
257where
258    IN: 'static,
259{
260    /// Create a callback that always returns the same value, ignoring input
261    pub fn always(value: OUT) -> Self
262    where
263        OUT: Clone + Send + Sync + 'static,
264    {
265        Self::constant(value)
266    }
267
268    /// Create a callback from a mutable closure using interior mutability
269    pub fn from_mut<F>(func: F) -> Self
270    where
271        F: FnMut(IN) -> OUT + Send + Sync + 'static,
272    {
273        use std::sync::Mutex;
274        let func = Mutex::new(func);
275        Self::from(move |input| {
276            let mut guard = func.lock().unwrap();
277            guard(input)
278        })
279    }
280}
281
282/// Internal state for tracking memoized callbacks
283struct CallbackState<IN, OUT> {
284    /// Previous dependencies for comparison
285    prev_deps: Option<Box<dyn EffectDependencies>>,
286    /// The underlying callback
287    callback: Option<Callback<IN, OUT>>,
288}
289
290impl<IN, OUT> CallbackState<IN, OUT> {
291    fn new() -> Self {
292        Self {
293            prev_deps: None,
294            callback: None,
295        }
296    }
297}
298
299/// Professional use_callback hook with dependency tracking
300///
301/// This hook memoizes a callback function and only recreates it when the dependencies change.
302/// This is useful for optimizing performance by preventing unnecessary re-renders of child
303/// components that depend on callback props.
304///
305/// Uses the same dependency system as effect hooks for consistency.
306///
307/// # Arguments
308///
309/// * `func` - The callback function to memoize
310/// * `deps` - Dependencies that the callback depends on (same as useEffect)
311///
312/// # Examples
313///
314/// ```rust,ignore
315/// use reratui::prelude::*;
316///
317/// #[component]
318/// fn MyComponent() -> Element {
319///     let (count, set_count) = use_state(|| 0);
320///     
321///     // Memoize with dependencies
322///     let on_click = use_callback(move |_| {
323///         set_count.update(|c| c + 1);
324///     }, (count,));
325///     
326///     rsx! {
327///         <Block title="Callback Example">
328///             <Paragraph>{format!("Count: {}", count)}</Paragraph>
329///         </Block>
330///     }
331/// }
332/// ```
333pub fn use_callback<IN, OUT, F, Deps>(func: F, deps: impl Into<Option<Deps>>) -> Callback<IN, OUT>
334where
335    F: Fn(IN) -> OUT + Send + Sync + Clone + 'static,
336    Deps: EffectDependencies + Clone + PartialEq + 'static,
337    IN: 'static,
338    OUT: 'static,
339{
340    let deps = deps.into();
341    with_hook_context(|ctx| {
342        let index = ctx.next_hook_index();
343        let state_ref = ctx.get_or_init_state(index, || CallbackState::<IN, OUT>::new());
344
345        let _should_recreate = {
346            let mut state = state_ref.borrow_mut();
347
348            // Determine if callback should be recreated
349            let should_recreate = match &deps {
350                None => {
351                    // No dependencies - only create once (like useCallback with empty deps)
352                    state.callback.is_none()
353                }
354                Some(current_deps) => {
355                    // Check if dependencies have changed
356                    match &state.prev_deps {
357                        None => {
358                            // First run - always create
359                            true
360                        }
361                        Some(prev_deps) => {
362                            // Compare dependencies using EffectDependencies trait
363                            !current_deps.deps_eq(prev_deps.as_ref())
364                        }
365                    }
366                }
367            };
368
369            if should_recreate {
370                // Create new callback directly from the function
371                let new_callback = Callback::from(func.clone());
372                state.callback = Some(new_callback);
373
374                // Store new dependencies
375                if let Some(current_deps) = &deps {
376                    state.prev_deps = Some(current_deps.clone_deps());
377                } else {
378                    state.prev_deps = None;
379                }
380            }
381
382            should_recreate
383        };
384
385        // Get the callback after releasing the mutable borrow
386        let state = state_ref.borrow();
387        (*state
388            .callback
389            .as_ref()
390            .expect("Callback should be initialized"))
391        .clone()
392    })
393}