leptos_motion_dom/
components.rs

1//! Motion Components for Leptos
2//!
3//! This module provides motion components that integrate with Leptos
4
5use crate::{
6    DragAxis,
7    DragConfig,
8    DragConstraints,
9    // animation_engine::{AnimationEngine, AnimationEngineBuilder}, // Unused
10    // easing_functions::*, // Unused
11    // repeat_config::{AnimationCycleManager, RepeatState}, // Unused
12    // transform_animations::{TransformAnimationBuilder, TransformAnimationManager}, // Unused
13};
14
15/// Type alias for momentum step callback
16type MomentumStepCallback = Rc<RefCell<Option<Box<dyn FnMut()>>>>;
17use leptos::prelude::{
18    Children, ClassAttribute, Effect, ElementChild, Get, GetUntracked, NodeRef, NodeRefAttribute,
19    OnAttribute, Set, StyleAttribute,
20};
21use leptos::reactive::signal::signal;
22use leptos::*;
23use leptos_motion_core::*;
24use std::cell::RefCell;
25use std::collections::HashMap;
26use std::rc::Rc;
27use wasm_bindgen::prelude::*;
28use web_sys;
29
30/// Animation target that can be either static or reactive
31#[derive(Clone)]
32pub enum AnimationTargetOrReactive {
33    /// Static animation target
34    Static(AnimationTarget),
35    /// Reactive animation target (function that returns AnimationTarget)
36    Reactive(Rc<dyn Fn() -> AnimationTarget>),
37}
38
39impl AnimationTargetOrReactive {
40    /// Get the current animation target
41    pub fn get_target(&self) -> AnimationTarget {
42        match self {
43            AnimationTargetOrReactive::Static(target) => target.clone(),
44            AnimationTargetOrReactive::Reactive(closure) => closure(),
45        }
46    }
47}
48
49/// Simple MotionDiv component for animated div elements
50#[component]
51pub fn MotionDiv(
52    /// CSS class name
53    #[prop(optional)]
54    class: Option<String>,
55    /// CSS styles
56    #[prop(optional)]
57    style: Option<String>,
58    /// Node reference for animation engine integration
59    #[prop(optional)]
60    node_ref: Option<NodeRef<leptos::html::Div>>,
61    /// Initial animation state
62    #[prop(optional)]
63    initial: Option<AnimationTarget>,
64    /// Target animation state
65    #[prop(optional)]
66    animate: Option<AnimationTarget>,
67    /// Transition configuration
68    #[prop(optional)]
69    _transition: Option<Transition>,
70    /// Hover animation state
71    #[prop(optional)]
72    while_hover: Option<AnimationTarget>,
73    /// Tap animation state
74    #[prop(optional)]
75    while_tap: Option<AnimationTarget>,
76    /// Layout animation enabled
77    #[prop(optional)]
78    _layout: Option<bool>,
79    /// Drag configuration
80    #[prop(optional)]
81    drag: Option<DragConfig>,
82    /// Drag constraints
83    #[prop(optional)]
84    _drag_constraints: Option<DragConstraints>,
85    /// Children elements
86    children: Children,
87) -> impl IntoView {
88    // Create signals for animation state
89    let (_is_hovered, _set_hovered) = signal(false);
90    let (_is_tapped, _set_tapped) = signal(false);
91    let (current_styles, set_styles) = signal(HashMap::<String, String>::new());
92
93    // Create signals for drag and momentum animation
94    let (is_dragging, set_dragging) = signal(false);
95    let (drag_position, set_drag_position) = signal((0.0, 0.0));
96    let (drag_velocity, set_drag_velocity) = signal((0.0, 0.0));
97    let (is_animating_momentum, set_animating_momentum) = signal(false);
98
99    // Create node reference if not provided
100    let node_ref = node_ref.unwrap_or_else(|| NodeRef::new());
101
102    // Initialize with initial styles
103    if let Some(initial_target) = initial {
104        let mut styles = HashMap::new();
105        for (key, value) in initial_target {
106            styles.insert(key, value.to_string_value());
107        }
108        set_styles.set(styles);
109    }
110
111    // โœ… CRITICAL FIX: Handle animate prop with proper signal tracking
112    if let Some(animate_target) = animate {
113        // Create a reactive effect that updates styles when animate target changes
114        Effect::new(move |_| {
115            let animate_values = animate_target.clone();
116            let mut styles = current_styles.get();
117            for (key, value) in animate_values.iter() {
118                styles.insert(key.clone(), value.to_string_value());
119            }
120            set_styles.set(styles);
121        });
122    }
123
124    // โœ… CRITICAL FIX: Handle while_hover prop with proper signal tracking
125    if let Some(hover_target) = while_hover {
126        let (hover_signal, _set_hover_signal) = signal(hover_target.clone());
127        Effect::new(move |_| {
128            let is_hovered = _is_hovered.get();
129            let hover_values = hover_signal.get();
130            let mut styles = current_styles.get();
131
132            if is_hovered {
133                for (key, value) in hover_values.iter() {
134                    styles.insert(key.clone(), value.to_string_value());
135                }
136            }
137            set_styles.set(styles);
138        });
139    }
140
141    // โœ… CRITICAL FIX: Handle while_tap prop with proper signal tracking
142    if let Some(tap_target) = while_tap {
143        let (tap_signal, _set_tap_signal) = signal(tap_target.clone());
144        Effect::new(move |_| {
145            let is_tapped = _is_tapped.get();
146            let tap_values = tap_signal.get();
147            let mut styles = current_styles.get();
148
149            if is_tapped {
150                for (key, value) in tap_values.iter() {
151                    styles.insert(key.clone(), value.to_string_value());
152                }
153            }
154            set_styles.set(styles);
155        });
156    }
157
158    // Convert styles to CSS string
159    let style_string = move || {
160        let mut styles = current_styles.get_untracked();
161
162        // Add drag position to styles
163        let (drag_x, drag_y) = drag_position.get_untracked();
164        if drag_x != 0.0 || drag_y != 0.0 {
165            styles.insert(
166                "transform".to_string(),
167                format!("translate({}px, {}px)", drag_x, drag_y),
168            );
169        }
170
171        // Combine with the style prop if provided
172        let mut style_parts = styles
173            .iter()
174            .map(|(key, value)| format!("{}: {}", key, value))
175            .collect::<Vec<_>>();
176
177        // Add the style prop if provided
178        if let Some(style_prop) = &style {
179            style_parts.push(style_prop.clone());
180        }
181
182        style_parts.join("; ")
183    };
184
185    // Clone drag config for use in multiple closures
186    let drag_config_clone = drag.clone();
187    let drag_config_mousemove = drag.clone();
188    let drag_config_mouseup = drag.clone();
189
190    // โœ… CRITICAL FIX: Add proper WASM memory management with cleanup
191    Effect::new(move |_| {
192        // This effect runs when the component is created and tracks all signals
193        // When the component is destroyed, this effect will be cleaned up automatically
194
195        // Track all animation-related signals to ensure proper reactivity
196        let _ = current_styles.get();
197        let _ = _is_hovered.get();
198        let _ = _is_tapped.get();
199        let _ = is_dragging.get();
200        let _ = drag_position.get();
201        let _ = drag_velocity.get();
202        let _ = is_animating_momentum.get();
203
204        // Return cleanup function (this will be called when the effect is destroyed)
205        move || {
206            // Cleanup any pending timeouts or animation frames
207            // The AnimationEngine already handles its own cleanup
208            web_sys::console::log_1(&"๐Ÿงน MotionDiv: Cleanup effect triggered".into());
209        }
210    });
211
212    view! {
213        <div
214            node_ref=node_ref
215            class=class
216            style=style_string()
217            on:mousedown=move |_event| {
218                if let Some(_drag_config) = &drag_config_clone {
219                    set_dragging.set(true);
220                    set_animating_momentum.set(false);
221                }
222            }
223            on:mousemove=move |event| {
224                if let Some(_drag_config) = &drag_config_mousemove && is_dragging.get() {
225                    let (current_x, current_y) = drag_position.get();
226                    let new_x = current_x + event.movement_x() as f64;
227                    let new_y = current_y + event.movement_y() as f64;
228                    set_drag_position.set((new_x, new_y));
229
230                    // Update velocity based on mouse movement
231                    let velocity_x = event.movement_x() as f64;
232                    let velocity_y = event.movement_y() as f64;
233                    set_drag_velocity.set((velocity_x, velocity_y));
234                }
235            }
236            on:mouseup=move |_event| {
237                if let Some(drag_config) = &drag_config_mouseup {
238                    set_dragging.set(false);
239
240                    // Start momentum animation if enabled
241                    if drag_config.momentum.unwrap_or(false) {
242                        set_animating_momentum.set(true);
243
244                        // Start momentum animation with proper continuous loop using Rc<RefCell<>>
245                        let start_momentum = move || {
246                            // Create a momentum step function using Rc<RefCell<>> to avoid circular references
247                            let momentum_step: MomentumStepCallback = Rc::new(RefCell::new(None));
248
249                            let momentum_step_ref = momentum_step.clone();
250                            let set_drag_position_clone = set_drag_position.clone();
251                            let set_drag_velocity_clone = set_drag_velocity.clone();
252                            let set_animating_momentum_clone = set_animating_momentum.clone();
253                            let drag_config_clone = drag_config.clone();
254                            let drag_position_clone = drag_position.clone();
255                            let drag_velocity_clone = drag_velocity.clone();
256                            let is_animating_momentum_clone = is_animating_momentum.clone();
257
258                            *momentum_step.borrow_mut() = Some(Box::new(move || {
259                                // Check if we should continue animating
260                                if !is_animating_momentum_clone.get() {
261                                    return;
262                                }
263
264                                let (current_x, current_y) = drag_position_clone.get();
265                                let (velocity_x, velocity_y) = drag_velocity_clone.get();
266
267                                // Apply friction (0.95 = 5% friction per frame)
268                                let friction = 0.95;
269                                let new_velocity_x = velocity_x * friction;
270                                let new_velocity_y = velocity_y * friction;
271
272                                // Update position based on velocity
273                                let new_x = current_x + new_velocity_x;
274                                let new_y = current_y + new_velocity_y;
275
276                                // Apply constraints during momentum with elastic behavior
277                                let (final_x, final_y) = if let Some(constraints) = &drag_config_clone.constraints {
278                                    let mut constrained_x = new_x;
279                                    let mut constrained_y = new_y;
280
281                                    // Apply axis constraints
282                                    match drag_config_clone.axis {
283                                        Some(DragAxis::X) => constrained_y = current_y,
284                                        Some(DragAxis::Y) => constrained_x = current_x,
285                                        _ => {} // Both or None - no axis constraint
286                                    }
287
288                                    // Apply boundary constraints with elastic behavior
289                                    let elastic_factor = drag_config_clone.elastic.unwrap_or(0.0);
290
291                                    if let Some(left) = constraints.left && constrained_x < left {
292                                        if elastic_factor > 0.0 {
293                                            let overshoot = left - constrained_x;
294                                            constrained_x = left - (overshoot * elastic_factor);
295                                        } else {
296                                            constrained_x = left;
297                                        }
298                                    }
299                                    if let Some(right) = constraints.right && constrained_x > right {
300                                        if elastic_factor > 0.0 {
301                                            let overshoot = constrained_x - right;
302                                            constrained_x = right + (overshoot * elastic_factor);
303                                        } else {
304                                            constrained_x = right;
305                                        }
306                                    }
307                                    if let Some(top) = constraints.top && constrained_y < top {
308                                        if elastic_factor > 0.0 {
309                                            let overshoot = top - constrained_y;
310                                            constrained_y = top - (overshoot * elastic_factor);
311                                        } else {
312                                            constrained_y = top;
313                                        }
314                                    }
315                                    if let Some(bottom) = constraints.bottom && constrained_y > bottom {
316                                        if elastic_factor > 0.0 {
317                                            let overshoot = constrained_y - bottom;
318                                            constrained_y = bottom + (overshoot * elastic_factor);
319                                        } else {
320                                            constrained_y = bottom;
321                                        }
322                                    }
323
324                                    (constrained_x, constrained_y)
325                                } else {
326                                    (new_x, new_y)
327                                };
328
329                                // Update position and velocity
330                                set_drag_position_clone.set((final_x, final_y));
331                                set_drag_velocity_clone.set((new_velocity_x, new_velocity_y));
332
333                                // Check if we should stop (velocity too low)
334                                let velocity_magnitude = (new_velocity_x * new_velocity_x + new_velocity_y * new_velocity_y).sqrt();
335                                if velocity_magnitude < 0.1 {
336                                    set_animating_momentum_clone.set(false);
337                                } else {
338                                    // Schedule next frame using a simple timeout
339                                    let momentum_step_ref = momentum_step_ref.clone();
340                                    let _ = web_sys::window()
341                                        .unwrap()
342                                        .set_timeout_with_callback_and_timeout_and_arguments_0(
343                                            &Closure::wrap(Box::new(move || {
344                                                // Call the momentum step function recursively
345                                                if let Some(ref mut step) = *momentum_step_ref.borrow_mut() {
346                                                    step();
347                                                }
348                                            }) as Box<dyn FnMut()>).as_ref().unchecked_ref(),
349                                            16 // ~60fps
350                                        )
351                                        .unwrap();
352                                }
353                            }));
354
355                            // Start the momentum animation
356                            if let Some(ref mut step) = *momentum_step.borrow_mut() {
357                                step();
358                            }
359                        };
360
361                        // Start the momentum animation
362                        start_momentum();
363                    }
364                }
365            }
366            on:mouseenter=move |_event| {
367                _set_hovered.set(true);
368            }
369            on:mouseleave=move |_event| {
370                _set_hovered.set(false);
371            }
372            on:click=move |_event| {
373                _set_tapped.set(true);
374                // Reset tap state after a short delay
375                let set_tapped_clone = _set_tapped.clone();
376                let _ = web_sys::window()
377                    .unwrap()
378                    .set_timeout_with_callback_and_timeout_and_arguments_0(
379                        &Closure::wrap(Box::new(move || {
380                            set_tapped_clone.set(false);
381                        }) as Box<dyn FnMut()>).as_ref().unchecked_ref(),
382                        150 // 150ms tap duration
383                    )
384                    .unwrap();
385            }
386        >
387            {children()}
388        </div>
389    }
390}
391
392/// Simple MotionSpan component for animated span elements
393#[component]
394pub fn MotionSpan(
395    /// CSS class name
396    #[prop(optional)]
397    class: Option<String>,
398    /// Initial animation state
399    #[prop(optional)]
400    initial: Option<AnimationTarget>,
401    /// Target animation state
402    #[prop(optional)]
403    animate: Option<AnimationTarget>,
404    /// Transition configuration
405    #[prop(optional)]
406    _transition: Option<Transition>,
407    /// Hover animation state
408    #[prop(optional)]
409    _while_hover: Option<AnimationTarget>,
410    /// Tap animation state
411    #[prop(optional)]
412    _while_tap: Option<AnimationTarget>,
413    /// Children elements
414    children: Children,
415) -> impl IntoView {
416    // Create signals for animation state
417    let (_is_hovered, _set_hovered) = signal(false);
418    let (_is_tapped, _set_tapped) = signal(false);
419    let (current_styles, set_styles) = signal(HashMap::<String, String>::new());
420
421    // Initialize with initial styles
422    if let Some(initial_target) = initial {
423        let mut styles = HashMap::new();
424        for (key, value) in initial_target {
425            styles.insert(key, value.to_string_value());
426        }
427        set_styles.set(styles);
428    }
429
430    // Handle animate prop
431    if let Some(animate_target) = animate {
432        let mut styles = current_styles.get();
433        for (key, value) in animate_target {
434            styles.insert(key, value.to_string_value());
435        }
436        set_styles.set(styles);
437    }
438
439    // Convert styles to CSS string
440    let style_string = move || {
441        let styles = current_styles.get();
442        styles
443            .iter()
444            .map(|(key, value)| format!("{}: {}", key, value))
445            .collect::<Vec<_>>()
446            .join("; ")
447    };
448
449    view! {
450        <span
451            class=class
452            style=style_string()
453        >
454            {children()}
455        </span>
456    }
457}