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, tessera};
26
27use crate::{
28    RippleState,
29    alignment::Alignment,
30    boxed::{BoxedArgsBuilder, boxed},
31    checkmark::{CheckmarkArgsBuilder, checkmark},
32    shape_def::Shape,
33    surface::{SurfaceArgsBuilder, surface},
34};
35
36#[derive(Clone, Default)]
37pub struct CheckboxState {
38    ripple: Arc<RippleState>,
39    checkmark: Arc<RwLock<CheckmarkState>>,
40}
41
42impl CheckboxState {
43    pub fn new(initial_state: bool) -> Self {
44        Self {
45            ripple: Default::default(),
46            checkmark: Arc::new(RwLock::new(CheckmarkState::new(initial_state))),
47        }
48    }
49}
50
51/// Arguments for the `checkbox` component.
52#[derive(Builder, Clone)]
53#[builder(pattern = "owned")]
54pub struct CheckboxArgs {
55    #[builder(default = "Arc::new(|_| {})")]
56    pub on_toggle: Arc<dyn Fn(bool) + Send + Sync>,
57
58    #[builder(default = "Dp(24.0)")]
59    pub size: Dp,
60
61    #[builder(default = "Color::new(0.8, 0.8, 0.8, 1.0)")]
62    pub color: Color,
63
64    #[builder(default = "Color::new(0.6, 0.7, 0.9, 1.0)")]
65    pub checked_color: Color,
66
67    #[builder(default = "Color::from_rgb_u8(119, 72, 146)")]
68    pub checkmark_color: Color,
69
70    #[builder(default = "5.0")]
71    pub checkmark_stroke_width: f32,
72
73    #[builder(default = "1.0")]
74    pub checkmark_animation_progress: f32,
75
76    #[builder(
77        default = "Shape::RoundedRectangle{ top_left: Dp(4.0), top_right: Dp(4.0), bottom_right: Dp(4.0), bottom_left: Dp(4.0), g2_k_value: 3.0 }"
78    )]
79    pub shape: Shape,
80
81    #[builder(default)]
82    pub hover_color: Option<Color>,
83}
84
85impl Default for CheckboxArgs {
86    fn default() -> Self {
87        CheckboxArgsBuilder::default().build().unwrap()
88    }
89}
90
91// Animation duration for the checkmark stroke (milliseconds)
92const CHECKMARK_ANIMATION_DURATION: Duration = Duration::from_millis(200);
93
94/// State for checkmark animation (similar to `SwitchState`)
95pub struct CheckmarkState {
96    pub checked: bool,
97    progress: f32,
98    last_toggle_time: Option<Instant>,
99}
100
101impl Default for CheckmarkState {
102    fn default() -> Self {
103        Self::new(false)
104    }
105}
106
107impl CheckmarkState {
108    pub fn new(initial_state: bool) -> Self {
109        Self {
110            checked: initial_state,
111            progress: if initial_state { 1.0 } else { 0.0 },
112            last_toggle_time: None,
113        }
114    }
115
116    /// Toggle checked state and start animation
117    pub fn toggle(&mut self) {
118        self.checked = !self.checked;
119        self.last_toggle_time = Some(Instant::now());
120    }
121
122    /// Update progress based on elapsed time
123    pub fn update_progress(&mut self) {
124        if let Some(start) = self.last_toggle_time {
125            let elapsed = start.elapsed();
126            let fraction =
127                (elapsed.as_secs_f32() / CHECKMARK_ANIMATION_DURATION.as_secs_f32()).min(1.0);
128            self.progress = if self.checked {
129                fraction
130            } else {
131                1.0 - fraction
132            };
133            if fraction >= 1.0 {
134                self.last_toggle_time = None; // Animation ends
135            }
136        }
137    }
138
139    pub fn progress(&self) -> f32 {
140        self.progress
141    }
142}
143
144/// Renders a checkbox component.
145///
146/// The checkbox is a standard UI element that allows users to select or deselect an option.
147/// It visually represents its state, typically as a square box that is either empty or contains a checkmark.
148/// The component handles its own animation and state transitions.
149///
150/// # Arguments
151///
152/// The component is configured by passing `CheckboxArgs` and a `CheckboxState`.
153///
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 parking_lot::RwLock;
163/// use tessera_ui_basic_components::checkbox::{checkbox, CheckboxArgs, CheckboxState, CheckmarkState};
164///
165/// // Create a checkbox that is initially unchecked.
166/// let unchecked_state = Arc::new(CheckboxState::default());
167/// checkbox(
168///     CheckboxArgs {
169///         on_toggle: Arc::new(|new_state| {
170///             // In a real app, you would update your state here.
171///             println!("Checkbox toggled to: {}", new_state);
172///         }),
173///         ..Default::default()
174///     },
175///     unchecked_state,
176/// );
177///
178/// // Create a checkbox that is initially checked.
179/// let checked_state = Arc::new(CheckboxState::new(true));
180/// checkbox(CheckboxArgs::default(), checked_state);
181/// ```
182#[tessera]
183pub fn checkbox(args: impl Into<CheckboxArgs>, state: Arc<CheckboxState>) {
184    let args: CheckboxArgs = args.into();
185
186    // If a state is provided, set up an updater to advance the animation each frame
187    let checkmark_state = state.checkmark.clone();
188    input_handler(Box::new(move |_input| {
189        checkmark_state.write().update_progress();
190    }));
191
192    // Click handler: toggle animation state if present, otherwise simply forward toggle callback
193    let on_click = {
194        let state = state.clone();
195        let on_toggle = args.on_toggle.clone();
196        Arc::new(move || {
197            state.checkmark.write().toggle();
198            on_toggle(state.checkmark.read().checked);
199        })
200    };
201
202    let ripple_state = state.ripple.clone();
203
204    surface(
205        SurfaceArgsBuilder::default()
206            .width(DimensionValue::Fixed(args.size.to_px()))
207            .height(DimensionValue::Fixed(args.size.to_px()))
208            .style(
209                if state.checkmark.read().checked {
210                    args.checked_color
211                } else {
212                    args.color
213                }
214                .into(),
215            )
216            .hover_style(args.hover_color.map(|c| c.into()))
217            .shape(args.shape)
218            .on_click(on_click)
219            .build()
220            .unwrap(),
221        Some(ripple_state),
222        {
223            let state_for_child = state.clone();
224            move || {
225                let progress = state_for_child.checkmark.read().progress();
226                if progress > 0.0 {
227                    surface(
228                        SurfaceArgsBuilder::default()
229                            .padding(Dp(2.0))
230                            .style(Color::TRANSPARENT.into())
231                            .build()
232                            .unwrap(),
233                        None,
234                        move || {
235                            boxed(
236                                BoxedArgsBuilder::default()
237                                    .alignment(Alignment::Center)
238                                    .build()
239                                    .unwrap(),
240                                |scope| {
241                                    scope.child(move || {
242                                        checkmark(
243                                            CheckmarkArgsBuilder::default()
244                                                .color(args.checkmark_color)
245                                                .stroke_width(args.checkmark_stroke_width)
246                                                .progress(progress)
247                                                .size(Dp(args.size.0 * 0.8))
248                                                .padding([2.0, 2.0])
249                                                .build()
250                                                .unwrap(),
251                                        )
252                                    });
253                                },
254                            );
255                        },
256                    )
257                }
258            }
259        },
260    );
261}