tessera_ui_basic_components/
glass_switch.rs1use std::{
2 sync::Arc,
3 time::{Duration, Instant},
4};
5
6use derive_builder::Builder;
7use parking_lot::Mutex;
8use tessera_ui::{
9 Color, ComputedData, Constraint, CursorEventContent, DimensionValue, Dp, PressKeyEventType,
10 PxPosition, winit::window::CursorIcon,
11};
12use tessera_ui_macros::tessera;
13
14use crate::{
15 fluid_glass::{FluidGlassArgsBuilder, GlassBorder, fluid_glass},
16 shape_def::Shape,
17};
18
19const ANIMATION_DURATION: Duration = Duration::from_millis(150);
20
21pub struct GlassSwitchState {
23 pub checked: bool,
24 progress: Mutex<f32>,
25 last_toggle_time: Mutex<Option<Instant>>,
26}
27
28impl GlassSwitchState {
29 pub fn new(initial_state: bool) -> Self {
30 Self {
31 checked: initial_state,
32 progress: Mutex::new(if initial_state { 1.0 } else { 0.0 }),
33 last_toggle_time: Mutex::new(None),
34 }
35 }
36
37 pub fn toggle(&mut self) {
38 self.checked = !self.checked;
39 *self.last_toggle_time.lock() = Some(Instant::now());
40 }
41}
42
43#[derive(Builder, Clone)]
44#[builder(pattern = "owned")]
45pub struct GlassSwitchArgs {
46 #[builder(default)]
47 pub state: Option<Arc<Mutex<GlassSwitchState>>>,
48
49 #[builder(default = "false")]
50 pub checked: bool,
51
52 #[builder(default = "Arc::new(|_| {})")]
53 pub on_toggle: Arc<dyn Fn(bool) + Send + Sync>,
54
55 #[builder(default = "Dp(52.0)")]
56 pub width: Dp,
57
58 #[builder(default = "Dp(32.0)")]
59 pub height: Dp,
60
61 #[builder(default = "Color::new(0.2, 0.7, 1.0, 0.5)")]
63 pub track_on_color: Color,
64 #[builder(default = "Color::new(0.8, 0.8, 0.8, 0.5)")]
66 pub track_off_color: Color,
67
68 #[builder(default = "0.5")]
70 pub thumb_on_alpha: f32,
71 #[builder(default = "1.0")]
73 pub thumb_off_alpha: f32,
74
75 #[builder(
77 default = "Some(GlassBorder::new(Dp(2.0), Color::BLUE.with_alpha(0.5)))",
78 setter(strip_option)
79 )]
80 pub thumb_border: Option<GlassBorder>,
81
82 #[builder(
84 default = "Some(GlassBorder::new(Dp(2.0), Color::WHITE.with_alpha(0.5)))",
85 setter(strip_option)
86 )]
87 pub track_border: Option<GlassBorder>,
88
89 #[builder(default = "Dp(3.0)")]
91 pub thumb_padding: Dp,
92}
93
94#[tessera]
95pub fn glass_switch(args: impl Into<GlassSwitchArgs>) {
96 let args: GlassSwitchArgs = args.into();
97 let thumb_size = Dp(args.height.0 - (args.thumb_padding.0 * 2.0));
98
99 let progress = args
101 .state
102 .as_ref()
103 .map(|s| *s.lock().progress.lock())
104 .unwrap_or(if args.checked { 1.0 } else { 0.0 });
105 let track_color = Color {
106 r: args.track_off_color.r + (args.track_on_color.r - args.track_off_color.r) * progress,
107 g: args.track_off_color.g + (args.track_on_color.g - args.track_off_color.g) * progress,
108 b: args.track_off_color.b + (args.track_on_color.b - args.track_off_color.b) * progress,
109 a: args.track_off_color.a + (args.track_on_color.a - args.track_off_color.a) * progress,
110 };
111 let mut arg = FluidGlassArgsBuilder::default()
112 .width(DimensionValue::Fixed(args.width.to_px()))
113 .height(DimensionValue::Fixed(args.height.to_px()))
114 .tint_color(track_color)
115 .blur_radius(10.0)
116 .shape(Shape::RoundedRectangle {
117 corner_radius: args.height.to_px().to_f32() / 2.0,
118 g2_k_value: 2.0,
119 })
120 .blur_radius(8.0);
121 if let Some(border) = args.track_border {
122 arg = arg.border(border);
123 }
124 let track_glass_arg = arg.build().unwrap();
125 fluid_glass(track_glass_arg, None, || {});
126
127 let thumb_alpha =
129 args.thumb_off_alpha + (args.thumb_on_alpha - args.thumb_off_alpha) * progress;
130 let thumb_color = Color::new(1.0, 1.0, 1.0, thumb_alpha);
131 let mut thumb_glass_arg = FluidGlassArgsBuilder::default()
132 .width(DimensionValue::Fixed(thumb_size.to_px()))
133 .height(DimensionValue::Fixed(thumb_size.to_px()))
134 .tint_color(thumb_color)
135 .refraction_height(1.0)
136 .shape(Shape::Ellipse);
137 if let Some(border) = args.thumb_border {
138 thumb_glass_arg = thumb_glass_arg.border(border);
139 }
140 let thumb_glass_arg = thumb_glass_arg.build().unwrap();
141 fluid_glass(thumb_glass_arg, None, || {});
142
143 let on_toggle = args.on_toggle.clone();
144 let state = args.state.clone();
145 let checked = args.checked;
146
147 state_handler(Box::new(move |input| {
148 if let Some(state) = &state {
149 let state = state.lock();
150 let mut progress = state.progress.lock();
151 if let Some(last_toggle_time) = *state.last_toggle_time.lock() {
152 let elapsed = last_toggle_time.elapsed();
153 let animation_fraction =
154 (elapsed.as_secs_f32() / ANIMATION_DURATION.as_secs_f32()).min(1.0);
155 *progress = if state.checked {
156 animation_fraction
157 } else {
158 1.0 - animation_fraction
159 };
160 }
161 }
162
163 let size = input.computed_data;
164 let is_cursor_in = if let Some(pos) = input.cursor_position {
165 pos.x.0 >= 0 && pos.x.0 < size.width.0 && pos.y.0 >= 0 && pos.y.0 < size.height.0
166 } else {
167 false
168 };
169 if is_cursor_in {
170 input.requests.cursor_icon = CursorIcon::Pointer;
171 }
172 for e in input.cursor_events.iter() {
173 if let CursorEventContent::Pressed(PressKeyEventType::Left) = &e.content {
174 if is_cursor_in {
175 if let Some(state) = &state {
176 state.lock().toggle();
177 }
178 on_toggle(!checked);
179 }
180 }
181 }
182 }));
183
184 measure(Box::new(move |input| {
185 let track_id = input.children_ids[0]; let thumb_id = input.children_ids[1]; let track_constraint = Constraint::new(
189 DimensionValue::Fixed(args.width.to_px()),
190 DimensionValue::Fixed(args.height.to_px()),
191 );
192 let thumb_constraint = Constraint::new(
193 DimensionValue::Wrap {
194 min: None,
195 max: None,
196 },
197 DimensionValue::Wrap {
198 min: None,
199 max: None,
200 },
201 );
202 let nodes_constraints = vec![(track_id, track_constraint), (thumb_id, thumb_constraint)];
204 let sizes_map = tessera_ui::measure_nodes(
205 nodes_constraints,
206 input.tree,
207 input.metadatas,
208 input.compute_resource_manager.clone(),
209 input.gpu,
210 );
211 let _track_size = sizes_map
212 .get(&track_id)
213 .and_then(|r| r.as_ref().ok())
214 .expect("track measurement failed");
215 let thumb_size = sizes_map
216 .get(&thumb_id)
217 .and_then(|r| r.as_ref().ok())
218 .expect("thumb measurement failed");
219 let self_width_px = args.width.to_px();
220 let self_height_px = args.height.to_px();
221 let thumb_padding_px = args.thumb_padding.to_px();
222 let progress = args
223 .state
224 .as_ref()
225 .map(|s| *s.lock().progress.lock())
226 .unwrap_or(if args.checked { 1.0 } else { 0.0 });
227 tessera_ui::place_node(
229 track_id,
230 PxPosition::new(tessera_ui::Px(0), tessera_ui::Px(0)),
231 input.metadatas,
232 );
233 let start_x = thumb_padding_px;
235 let end_x = self_width_px - thumb_size.width - thumb_padding_px;
236 let thumb_x = start_x.0 as f32 + (end_x.0 - start_x.0) as f32 * progress;
237 let thumb_y = (self_height_px - thumb_size.height) / 2;
238 tessera_ui::place_node(
239 thumb_id,
240 PxPosition::new(tessera_ui::Px(thumb_x as i32), thumb_y),
241 input.metadatas,
242 );
243 Ok(ComputedData {
244 width: self_width_px,
245 height: self_height_px,
246 })
247 }));
248}