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}