tessera_ui_basic_components/
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 pipelines::ShapeCommand,
16 shape_def::Shape,
17 surface::{SurfaceArgsBuilder, surface},
18};
19
20const ANIMATION_DURATION: Duration = Duration::from_millis(150);
21
22pub struct SwitchState {
24 pub checked: bool,
25 progress: Mutex<f32>,
26 last_toggle_time: Mutex<Option<Instant>>,
27}
28
29impl SwitchState {
30 pub fn new(initial_state: bool) -> Self {
31 Self {
32 checked: initial_state,
33 progress: Mutex::new(if initial_state { 1.0 } else { 0.0 }),
34 last_toggle_time: Mutex::new(None),
35 }
36 }
37
38 pub fn toggle(&mut self) {
39 self.checked = !self.checked;
40 *self.last_toggle_time.lock() = Some(Instant::now());
41 }
42}
43
44#[derive(Builder, Clone)]
46#[builder(pattern = "owned")]
47pub struct SwitchArgs {
48 #[builder(default)]
49 pub state: Option<Arc<Mutex<SwitchState>>>,
50
51 #[builder(default = "false")]
52 pub checked: bool,
53
54 #[builder(default = "Arc::new(|_| {})")]
55 pub on_toggle: Arc<dyn Fn(bool) + Send + Sync>,
56
57 #[builder(default = "Dp(52.0)")]
58 pub width: Dp,
59
60 #[builder(default = "Dp(32.0)")]
61 pub height: Dp,
62
63 #[builder(default = "Color::new(0.8, 0.8, 0.8, 1.0)")]
64 pub track_color: Color,
65
66 #[builder(default = "Color::new(0.6, 0.7, 0.9, 1.0)")]
67 pub track_checked_color: Color,
68
69 #[builder(default = "Color::WHITE")]
70 pub thumb_color: Color,
71
72 #[builder(default = "Dp(3.0)")]
73 pub thumb_padding: Dp,
74}
75
76#[tessera]
77pub fn switch(args: impl Into<SwitchArgs>) {
78 let args: SwitchArgs = args.into();
79 let thumb_size = Dp(args.height.0 - (args.thumb_padding.0 * 2.0));
80
81 surface(
82 SurfaceArgsBuilder::default()
83 .width(DimensionValue::Fixed(thumb_size.to_px()))
84 .height(DimensionValue::Fixed(thumb_size.to_px()))
85 .color(args.thumb_color)
86 .shape(Shape::Ellipse)
87 .build()
88 .unwrap(),
89 None,
90 || {},
91 );
92
93 let on_toggle = args.on_toggle.clone();
94 let state = args.state.clone();
95 let checked = args.checked;
96
97 state_handler(Box::new(move |input| {
98 if let Some(state) = &state {
99 let state = state.lock();
100 let mut progress = state.progress.lock();
101
102 if let Some(last_toggle_time) = *state.last_toggle_time.lock() {
103 let elapsed = last_toggle_time.elapsed();
104 let animation_fraction =
105 (elapsed.as_secs_f32() / ANIMATION_DURATION.as_secs_f32()).min(1.0);
106
107 *progress = if state.checked {
108 animation_fraction
109 } else {
110 1.0 - animation_fraction
111 };
112 }
113 }
114
115 let size = input.computed_data;
116 let is_cursor_in = if let Some(pos) = input.cursor_position {
117 pos.x.0 >= 0 && pos.x.0 < size.width.0 && pos.y.0 >= 0 && pos.y.0 < size.height.0
118 } else {
119 false
120 };
121
122 if is_cursor_in {
123 input.requests.cursor_icon = CursorIcon::Pointer;
124 }
125
126 for e in input.cursor_events.iter() {
127 if let CursorEventContent::Pressed(PressKeyEventType::Left) = &e.content {
128 if is_cursor_in {
129 on_toggle(!checked);
130 }
131 }
132 }
133 }));
134
135 measure(Box::new(move |input| {
136 let thumb_id = input.children_ids[0];
137 let thumb_constraint = Constraint::new(
138 DimensionValue::Wrap {
139 min: None,
140 max: None,
141 },
142 DimensionValue::Wrap {
143 min: None,
144 max: None,
145 },
146 );
147 let thumb_size = tessera_ui::measure_node(
148 thumb_id,
149 &thumb_constraint,
150 input.tree,
151 input.metadatas,
152 input.compute_resource_manager.clone(),
153 input.gpu,
154 )?;
155
156 let self_width_px = args.width.to_px();
157 let self_height_px = args.height.to_px();
158 let thumb_padding_px = args.thumb_padding.to_px();
159
160 let progress = args
161 .state
162 .as_ref()
163 .map(|s| *s.lock().progress.lock())
164 .unwrap_or(if args.checked { 1.0 } else { 0.0 });
165
166 let start_x = thumb_padding_px;
167 let end_x = self_width_px - thumb_size.width - thumb_padding_px;
168 let thumb_x = start_x.0 as f32 + (end_x.0 - start_x.0) as f32 * progress;
169
170 let thumb_y = (self_height_px - thumb_size.height) / 2;
171
172 tessera_ui::place_node(
173 thumb_id,
174 PxPosition::new(tessera_ui::Px(thumb_x as i32), thumb_y),
175 input.metadatas,
176 );
177
178 let track_color = if args.checked {
179 args.track_checked_color
180 } else {
181 args.track_color
182 };
183 let track_command = ShapeCommand::Rect {
184 color: track_color,
185 corner_radius: (self_height_px.0 as f32) / 2.0,
186 g2_k_value: 2.0, shadow: None,
188 };
189 if let Some(mut metadata) = input.metadatas.get_mut(&input.current_node_id) {
190 metadata.push_draw_command(track_command);
191 }
192
193 Ok(ComputedData {
194 width: self_width_px,
195 height: self_height_px,
196 })
197 }));
198}