tessera_ui_basic_components/
checkbox.rs

1use std::{
2    sync::Arc,
3    time::{Duration, Instant},
4};
5
6use derive_builder::Builder;
7use parking_lot::RwLock;
8use tessera_ui::{Color, DimensionValue, Dp};
9use tessera_ui_macros::tessera;
10
11use crate::{
12    alignment::Alignment,
13    boxed::{BoxedArgs, boxed_ui},
14    checkmark::{CheckmarkArgsBuilder, checkmark},
15    shape_def::Shape,
16    surface::{SurfaceArgsBuilder, surface},
17};
18
19#[derive(Clone)]
20pub struct CheckboxState {
21    pub ripple: Arc<crate::ripple_state::RippleState>,
22    pub checkmark: Arc<RwLock<CheckmarkState>>,
23}
24
25impl CheckboxState {
26    pub fn new(checked: bool) -> Self {
27        Self {
28            ripple: Arc::new(crate::ripple_state::RippleState::new()),
29            checkmark: Arc::new(RwLock::new(CheckmarkState::new(checked))),
30        }
31    }
32}
33
34/// Arguments for the `checkbox` component.
35#[derive(Builder, Clone)]
36#[builder(pattern = "owned")]
37pub struct CheckboxArgs {
38    #[builder(default)]
39    pub checked: bool,
40
41    #[builder(default = "Arc::new(|_| {})")]
42    pub on_toggle: Arc<dyn Fn(bool) + Send + Sync>,
43
44    #[builder(default = "Dp(24.0)")]
45    pub size: Dp,
46
47    #[builder(default = "Color::new(0.8, 0.8, 0.8, 1.0)")]
48    pub color: Color,
49
50    #[builder(default = "Color::new(0.6, 0.7, 0.9, 1.0)")]
51    pub checked_color: Color,
52
53    #[builder(default = "Color::from_rgb_u8(119, 72, 146)")]
54    pub checkmark_color: Color,
55
56    #[builder(default = "5.0")]
57    pub checkmark_stroke_width: f32,
58
59    #[builder(default = "1.0")]
60    pub checkmark_animation_progress: f32,
61
62    #[builder(default = "Shape::RoundedRectangle{ corner_radius: 4.0, g2_k_value: 3.0 }")]
63    pub shape: Shape,
64
65    #[builder(default)]
66    pub hover_color: Option<Color>,
67
68    #[builder(default = "None")]
69    pub state: Option<Arc<CheckboxState>>,
70}
71
72impl Default for CheckboxArgs {
73    fn default() -> Self {
74        CheckboxArgsBuilder::default().build().unwrap()
75    }
76}
77
78// Animation duration for the checkmark stroke (milliseconds)
79const CHECKMARK_ANIMATION_DURATION: Duration = Duration::from_millis(200);
80
81/// State for checkmark animation (similar风格 to `SwitchState`)
82pub struct CheckmarkState {
83    pub checked: bool,
84    progress: f32,
85    last_toggle_time: Option<Instant>,
86}
87
88impl CheckmarkState {
89    pub fn new(initial_state: bool) -> Self {
90        Self {
91            checked: initial_state,
92            progress: if initial_state { 1.0 } else { 0.0 },
93            last_toggle_time: None,
94        }
95    }
96
97    /// Toggle checked state and start animation
98    pub fn toggle(&mut self) {
99        self.checked = !self.checked;
100        self.last_toggle_time = Some(Instant::now());
101    }
102
103    /// Update progress based on elapsed time
104    pub fn update_progress(&mut self) {
105        if let Some(start) = self.last_toggle_time {
106            let elapsed = start.elapsed();
107            let fraction =
108                (elapsed.as_secs_f32() / CHECKMARK_ANIMATION_DURATION.as_secs_f32()).min(1.0);
109            self.progress = if self.checked {
110                fraction
111            } else {
112                1.0 - fraction
113            };
114            if fraction >= 1.0 {
115                self.last_toggle_time = None; // Animation ends
116            }
117        }
118    }
119
120    pub fn progress(&self) -> f32 {
121        self.progress
122    }
123}
124
125#[tessera]
126pub fn checkbox(args: impl Into<CheckboxArgs>) {
127    let args: CheckboxArgs = args.into();
128
129    // Optional external animation state, similar to Switch component pattern
130    let state = args.state.clone();
131
132    // If a state is provided, set up an updater to advance the animation each frame
133    if let Some(state_for_handler) = state.clone() {
134        let checkmark_state = state_for_handler.checkmark.clone();
135        state_handler(Box::new(move |_input| {
136            checkmark_state.write().update_progress();
137        }));
138    }
139
140    // Click handler: toggle animation state if present, otherwise simply forward toggle callback
141    let on_click = {
142        let state = state.clone();
143        let on_toggle = args.on_toggle.clone();
144        let checked_initial = args.checked;
145        Arc::new(move || {
146            if let Some(state) = &state {
147                state.checkmark.write().toggle();
148                on_toggle(state.checkmark.read().checked);
149            } else {
150                // Fallback: no internal animation state, just invert checked value
151                on_toggle(!checked_initial);
152            }
153        })
154    };
155
156    let ripple_state = state.as_ref().map(|s| s.ripple.clone());
157
158    surface(
159        SurfaceArgsBuilder::default()
160            .width(DimensionValue::Fixed(args.size.to_px()))
161            .height(DimensionValue::Fixed(args.size.to_px()))
162            .color(if args.checked {
163                args.checked_color
164            } else {
165                args.color
166            })
167            .hover_color(args.hover_color)
168            .shape(args.shape)
169            .on_click(Some(on_click))
170            .build()
171            .unwrap(),
172        ripple_state,
173        {
174            let state_for_child = state.clone();
175            move || {
176                let progress = state_for_child
177                    .as_ref()
178                    .map(|s| s.checkmark.read().progress())
179                    .unwrap_or(if args.checked { 1.0 } else { 0.0 });
180                if progress > 0.0 {
181                    surface(
182                        SurfaceArgsBuilder::default()
183                            .padding(Dp(2.0))
184                            .color(Color::TRANSPARENT)
185                            .build()
186                            .unwrap(),
187                        None,
188                        move || {
189                            boxed_ui!(
190                                BoxedArgs {
191                                    alignment: Alignment::Center,
192                                    ..Default::default()
193                                },
194                                move || checkmark(
195                                    CheckmarkArgsBuilder::default()
196                                        .color(args.checkmark_color)
197                                        .stroke_width(args.checkmark_stroke_width)
198                                        .progress(progress)
199                                        .size(Dp(args.size.0 * 0.7))
200                                        .padding([5.0, 5.0])
201                                        .build()
202                                        .unwrap()
203                                )
204                            );
205                        },
206                    )
207                }
208            }
209        },
210    );
211}