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}