tessera_ui_basic_components/
checkbox.rs1use 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#[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
78const CHECKMARK_ANIMATION_DURATION: Duration = Duration::from_millis(200);
80
81pub 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 pub fn toggle(&mut self) {
99 self.checked = !self.checked;
100 self.last_toggle_time = Some(Instant::now());
101 }
102
103 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; }
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 let state = args.state.clone();
131
132 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 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 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}