tessera_ui_basic_components/
glass_switch.rs1#![allow(clippy::needless_pass_by_value)]
2use std::{
13 sync::Arc,
14 time::{Duration, Instant},
15};
16
17use derive_builder::Builder;
18use parking_lot::Mutex;
19use tessera_ui::{
20 Color, ComputedData, Constraint, CursorEventContent, DimensionValue, Dp, PressKeyEventType,
21 PxPosition, winit::window::CursorIcon,
22};
23use tessera_ui_macros::tessera;
24
25use crate::{
26 animation,
27 fluid_glass::{FluidGlassArgsBuilder, GlassBorder, fluid_glass},
28 shape_def::Shape,
29};
30
31const ANIMATION_DURATION: Duration = Duration::from_millis(150);
32
33pub struct GlassSwitchState {
35 pub checked: bool,
36 progress: Mutex<f32>,
37 last_toggle_time: Mutex<Option<Instant>>,
38}
39
40impl GlassSwitchState {
41 pub fn new(initial_state: bool) -> Self {
42 Self {
43 checked: initial_state,
44 progress: Mutex::new(if initial_state { 1.0 } else { 0.0 }),
45 last_toggle_time: Mutex::new(None),
46 }
47 }
48
49 pub fn toggle(&mut self) {
50 self.checked = !self.checked;
51 *self.last_toggle_time.lock() = Some(Instant::now());
52 }
53}
54
55#[derive(Builder, Clone)]
56#[builder(pattern = "owned")]
57pub struct GlassSwitchArgs {
58 #[builder(default)]
59 pub state: Option<Arc<Mutex<GlassSwitchState>>>,
60
61 #[builder(default = "false")]
62 pub checked: bool,
63
64 #[builder(default = "Arc::new(|_| {})")]
65 pub on_toggle: Arc<dyn Fn(bool) + Send + Sync>,
66
67 #[builder(default = "Dp(52.0)")]
68 pub width: Dp,
69
70 #[builder(default = "Dp(32.0)")]
71 pub height: Dp,
72
73 #[builder(default = "Color::new(0.2, 0.7, 1.0, 0.5)")]
75 pub track_on_color: Color,
76 #[builder(default = "Color::new(0.8, 0.8, 0.8, 0.5)")]
78 pub track_off_color: Color,
79
80 #[builder(default = "0.5")]
82 pub thumb_on_alpha: f32,
83 #[builder(default = "1.0")]
85 pub thumb_off_alpha: f32,
86
87 #[builder(default, setter(strip_option))]
89 pub thumb_border: Option<GlassBorder>,
90
91 #[builder(default, setter(strip_option))]
93 pub track_border: Option<GlassBorder>,
94
95 #[builder(default = "Dp(3.0)")]
97 pub thumb_padding: Dp,
98}
99
100impl Default for GlassSwitchArgs {
101 fn default() -> Self {
102 GlassSwitchArgsBuilder::default().build().unwrap()
103 }
104}
105
106#[tessera]
107pub fn glass_switch(args: impl Into<GlassSwitchArgs>) {
150 let args: GlassSwitchArgs = args.into();
151 let thumb_size = Dp(args.height.0 - (args.thumb_padding.0 * 2.0));
152
153 let progress = args
155 .state
156 .as_ref()
157 .map(|s| *s.lock().progress.lock())
158 .unwrap_or(if args.checked { 1.0 } else { 0.0 });
159 let track_color = Color {
160 r: args.track_off_color.r + (args.track_on_color.r - args.track_off_color.r) * progress,
161 g: args.track_off_color.g + (args.track_on_color.g - args.track_off_color.g) * progress,
162 b: args.track_off_color.b + (args.track_on_color.b - args.track_off_color.b) * progress,
163 a: args.track_off_color.a + (args.track_on_color.a - args.track_off_color.a) * progress,
164 };
165 let mut arg = FluidGlassArgsBuilder::default()
166 .width(DimensionValue::Fixed(args.width.to_px()))
167 .height(DimensionValue::Fixed(args.height.to_px()))
168 .tint_color(track_color)
169 .blur_radius(10.0)
170 .shape(Shape::RoundedRectangle {
171 corner_radius: args.height.to_px().to_f32() / 2.0,
172 g2_k_value: 2.0,
173 })
174 .blur_radius(8.0);
175 if let Some(border) = args.track_border {
176 arg = arg.border(border);
177 }
178 let track_glass_arg = arg.build().unwrap();
179 fluid_glass(track_glass_arg, None, || {});
180
181 let thumb_alpha =
183 args.thumb_off_alpha + (args.thumb_on_alpha - args.thumb_off_alpha) * progress;
184 let thumb_color = Color::new(1.0, 1.0, 1.0, thumb_alpha);
185 let mut thumb_glass_arg = FluidGlassArgsBuilder::default()
186 .width(DimensionValue::Fixed(thumb_size.to_px()))
187 .height(DimensionValue::Fixed(thumb_size.to_px()))
188 .tint_color(thumb_color)
189 .refraction_height(1.0)
190 .shape(Shape::Ellipse);
191 if let Some(border) = args.thumb_border {
192 thumb_glass_arg = thumb_glass_arg.border(border);
193 }
194 let thumb_glass_arg = thumb_glass_arg.build().unwrap();
195 fluid_glass(thumb_glass_arg, None, || {});
196
197 let on_toggle = args.on_toggle.clone();
198 let state = args.state.clone();
199 let checked = args.checked;
200
201 state_handler(Box::new(move |input| {
202 if let Some(state) = &state {
203 let state = state.lock();
204 let mut progress = state.progress.lock();
205 if let Some(last_toggle_time) = *state.last_toggle_time.lock() {
206 let elapsed = last_toggle_time.elapsed();
207 let animation_fraction =
208 (elapsed.as_secs_f32() / ANIMATION_DURATION.as_secs_f32()).min(1.0);
209 *progress = if state.checked {
210 animation_fraction
211 } else {
212 1.0 - animation_fraction
213 };
214 }
215 }
216
217 let size = input.computed_data;
218 let is_cursor_in = if let Some(pos) = input.cursor_position_rel {
219 pos.x.0 >= 0 && pos.x.0 < size.width.0 && pos.y.0 >= 0 && pos.y.0 < size.height.0
220 } else {
221 false
222 };
223 if is_cursor_in {
224 input.requests.cursor_icon = CursorIcon::Pointer;
225 }
226 for e in input.cursor_events.iter() {
227 if let CursorEventContent::Pressed(PressKeyEventType::Left) = &e.content {
228 if is_cursor_in {
229 if let Some(state) = &state {
230 state.lock().toggle();
231 }
232 on_toggle(!checked);
233 }
234 }
235 }
236 }));
237
238 measure(Box::new(move |input| {
239 let track_id = input.children_ids[0]; let thumb_id = input.children_ids[1]; let track_constraint = Constraint::new(
243 DimensionValue::Fixed(args.width.to_px()),
244 DimensionValue::Fixed(args.height.to_px()),
245 );
246 let thumb_constraint = Constraint::new(
247 DimensionValue::Wrap {
248 min: None,
249 max: None,
250 },
251 DimensionValue::Wrap {
252 min: None,
253 max: None,
254 },
255 );
256 let nodes_constraints = vec![(track_id, track_constraint), (thumb_id, thumb_constraint)];
258 let sizes_map = input.measure_children(nodes_constraints)?;
259
260 let _track_size = sizes_map.get(&track_id).unwrap();
261 let thumb_size = sizes_map.get(&thumb_id).unwrap();
262 let self_width_px = args.width.to_px();
263 let self_height_px = args.height.to_px();
264 let thumb_padding_px = args.thumb_padding.to_px();
265
266 let progress = animation::easing(
267 args.state
268 .as_ref()
269 .map(|s| *s.lock().progress.lock())
270 .unwrap_or(if args.checked { 1.0 } else { 0.0 }),
271 );
272 input.place_child(
274 track_id,
275 PxPosition::new(tessera_ui::Px(0), tessera_ui::Px(0)),
276 );
277 let start_x = thumb_padding_px;
279 let end_x = self_width_px - thumb_size.width - thumb_padding_px;
280 let thumb_x = start_x.0 as f32 + (end_x.0 - start_x.0) as f32 * progress;
281 let thumb_y = (self_height_px - thumb_size.height) / 2;
282 input.place_child(
283 thumb_id,
284 PxPosition::new(tessera_ui::Px(thumb_x as i32), thumb_y),
285 );
286 Ok(ComputedData {
287 width: self_width_px,
288 height: self_height_px,
289 })
290 }));
291}