tessera_ui_basic_components/
checkbox.rs

1//! A customizable, animated checkbox UI component for Tessera UI.
2//!
3//! This module provides a standard checkbox widget with support for animated checkmark transitions,
4//! external or internal state management, and flexible styling options. The checkbox can be used
5//! wherever a boolean selection is required, such as forms, settings panels, or interactive lists.
6//!
7//! Features include:
8//! - Smooth checkmark animation on toggle
9//! - Optional external state for advanced control and animation
10//! - Customizable size, colors, shape, and hover effects
11//! - Callback for state changes to integrate with application logic
12//!
13//! Typical usage involves passing [`CheckboxArgs`] to the [`checkbox`] function, with optional
14//! state sharing for animation or controlled components.
15//!
16//! Suitable for both simple and complex UI scenarios requiring a responsive, visually appealing checkbox.
17
18use std::{
19    sync::Arc,
20    time::{Duration, Instant},
21};
22
23use derive_builder::Builder;
24use parking_lot::RwLock;
25use tessera_ui::{Color, DimensionValue, Dp};
26use tessera_ui_macros::tessera;
27
28use crate::{
29    alignment::Alignment,
30    boxed::{BoxedArgs, boxed_ui},
31    checkmark::{CheckmarkArgsBuilder, checkmark},
32    shape_def::Shape,
33    surface::{SurfaceArgsBuilder, surface},
34};
35
36#[derive(Clone)]
37pub struct CheckboxState {
38    pub ripple: Arc<crate::ripple_state::RippleState>,
39    pub checkmark: Arc<RwLock<CheckmarkState>>,
40}
41
42impl CheckboxState {
43    pub fn new(checked: bool) -> Self {
44        Self {
45            ripple: Arc::new(crate::ripple_state::RippleState::new()),
46            checkmark: Arc::new(RwLock::new(CheckmarkState::new(checked))),
47        }
48    }
49}
50
51/// Arguments for the `checkbox` component.
52#[derive(Builder, Clone)]
53#[builder(pattern = "owned")]
54pub struct CheckboxArgs {
55    #[builder(default)]
56    pub checked: bool,
57
58    #[builder(default = "Arc::new(|_| {})")]
59    pub on_toggle: Arc<dyn Fn(bool) + Send + Sync>,
60
61    #[builder(default = "Dp(24.0)")]
62    pub size: Dp,
63
64    #[builder(default = "Color::new(0.8, 0.8, 0.8, 1.0)")]
65    pub color: Color,
66
67    #[builder(default = "Color::new(0.6, 0.7, 0.9, 1.0)")]
68    pub checked_color: Color,
69
70    #[builder(default = "Color::from_rgb_u8(119, 72, 146)")]
71    pub checkmark_color: Color,
72
73    #[builder(default = "5.0")]
74    pub checkmark_stroke_width: f32,
75
76    #[builder(default = "1.0")]
77    pub checkmark_animation_progress: f32,
78
79    #[builder(default = "Shape::RoundedRectangle{ corner_radius: 4.0, g2_k_value: 3.0 }")]
80    pub shape: Shape,
81
82    #[builder(default)]
83    pub hover_color: Option<Color>,
84
85    #[builder(default = "None")]
86    pub state: Option<Arc<CheckboxState>>,
87}
88
89impl Default for CheckboxArgs {
90    fn default() -> Self {
91        CheckboxArgsBuilder::default().build().unwrap()
92    }
93}
94
95// Animation duration for the checkmark stroke (milliseconds)
96const CHECKMARK_ANIMATION_DURATION: Duration = Duration::from_millis(200);
97
98/// State for checkmark animation (similar风格 to `SwitchState`)
99pub struct CheckmarkState {
100    pub checked: bool,
101    progress: f32,
102    last_toggle_time: Option<Instant>,
103}
104
105impl CheckmarkState {
106    pub fn new(initial_state: bool) -> Self {
107        Self {
108            checked: initial_state,
109            progress: if initial_state { 1.0 } else { 0.0 },
110            last_toggle_time: None,
111        }
112    }
113
114    /// Toggle checked state and start animation
115    pub fn toggle(&mut self) {
116        self.checked = !self.checked;
117        self.last_toggle_time = Some(Instant::now());
118    }
119
120    /// Update progress based on elapsed time
121    pub fn update_progress(&mut self) {
122        if let Some(start) = self.last_toggle_time {
123            let elapsed = start.elapsed();
124            let fraction =
125                (elapsed.as_secs_f32() / CHECKMARK_ANIMATION_DURATION.as_secs_f32()).min(1.0);
126            self.progress = if self.checked {
127                fraction
128            } else {
129                1.0 - fraction
130            };
131            if fraction >= 1.0 {
132                self.last_toggle_time = None; // Animation ends
133            }
134        }
135    }
136
137    pub fn progress(&self) -> f32 {
138        self.progress
139    }
140}
141
142/// Renders a checkbox component.
143///
144/// The checkbox is a standard UI element that allows users to select or deselect an option.
145/// It visually represents its state, typically as a square box that is either empty or contains a checkmark.
146/// The component handles its own animation and state transitions when an optional `CheckboxState` is provided.
147///
148/// # Arguments
149///
150/// The component is configured by passing `CheckboxArgs`.
151///
152/// * `checked`: A `bool` indicating whether the checkbox is currently checked. This determines its
153///   visual appearance.
154/// * `on_toggle`: A callback function `Arc<dyn Fn(bool) + Send + Sync>` that is invoked when the user
155///   clicks the checkbox. It receives the new `checked` state as an argument, allowing the
156///   application state to be updated.
157///
158/// # Example
159///
160/// ```
161/// use std::sync::Arc;
162/// use tessera_ui_basic_components::checkbox::{checkbox, CheckboxArgs};
163///
164/// // Create a checkbox that is initially unchecked.
165/// // The `on_toggle` callback is triggered when the user clicks it.
166/// checkbox(CheckboxArgs {
167///     checked: false,
168///     on_toggle: Arc::new(|new_state| {
169///         // In a real app, you would update your state here.
170///         println!("Checkbox toggled to: {}", new_state);
171///     }),
172///     ..Default::default()
173/// });
174///
175/// // Create a checkbox that is initially checked.
176/// checkbox(CheckboxArgs {
177///     checked: true,
178///     ..Default::default()
179/// });
180/// ```
181#[tessera]
182pub fn checkbox(args: impl Into<CheckboxArgs>) {
183    let args: CheckboxArgs = args.into();
184
185    // Optional external animation state, similar to Switch component pattern
186    let state = args.state.clone();
187
188    // If a state is provided, set up an updater to advance the animation each frame
189    if let Some(state_for_handler) = state.clone() {
190        let checkmark_state = state_for_handler.checkmark.clone();
191        state_handler(Box::new(move |_input| {
192            checkmark_state.write().update_progress();
193        }));
194    }
195
196    // Click handler: toggle animation state if present, otherwise simply forward toggle callback
197    let on_click = {
198        let state = state.clone();
199        let on_toggle = args.on_toggle.clone();
200        let checked_initial = args.checked;
201        Arc::new(move || {
202            if let Some(state) = &state {
203                state.checkmark.write().toggle();
204                on_toggle(state.checkmark.read().checked);
205            } else {
206                // Fallback: no internal animation state, just invert checked value
207                on_toggle(!checked_initial);
208            }
209        })
210    };
211
212    let ripple_state = state.as_ref().map(|s| s.ripple.clone());
213
214    surface(
215        SurfaceArgsBuilder::default()
216            .width(DimensionValue::Fixed(args.size.to_px()))
217            .height(DimensionValue::Fixed(args.size.to_px()))
218            .color(if args.checked {
219                args.checked_color
220            } else {
221                args.color
222            })
223            .hover_color(args.hover_color)
224            .shape(args.shape)
225            .on_click(Some(on_click))
226            .build()
227            .unwrap(),
228        ripple_state,
229        {
230            let state_for_child = state.clone();
231            move || {
232                let progress = state_for_child
233                    .as_ref()
234                    .map(|s| s.checkmark.read().progress())
235                    .unwrap_or(if args.checked { 1.0 } else { 0.0 });
236                if progress > 0.0 {
237                    surface(
238                        SurfaceArgsBuilder::default()
239                            .padding(Dp(2.0))
240                            .color(Color::TRANSPARENT)
241                            .build()
242                            .unwrap(),
243                        None,
244                        move || {
245                            boxed_ui!(
246                                BoxedArgs {
247                                    alignment: Alignment::Center,
248                                    ..Default::default()
249                                },
250                                move || checkmark(
251                                    CheckmarkArgsBuilder::default()
252                                        .color(args.checkmark_color)
253                                        .stroke_width(args.checkmark_stroke_width)
254                                        .progress(progress)
255                                        .size(Dp(args.size.0 * 0.8))
256                                        .padding([2.0, 2.0])
257                                        .build()
258                                        .unwrap()
259                                )
260                            );
261                        },
262                    )
263                }
264            }
265        },
266    );
267}