tessera_ui_basic_components/
slider.rs1use std::sync::Arc;
16
17use derive_builder::Builder;
18use parking_lot::RwLock;
19use tessera_ui::{
20 Color, ComputedData, Constraint, CursorEventContent, DimensionValue, Dp, InputHandlerInput,
21 MeasureInput, MeasurementError, Px, PxPosition, focus_state::Focus, tessera,
22 winit::window::CursorIcon,
23};
24
25use crate::{
26 shape_def::Shape,
27 surface::{SurfaceArgsBuilder, surface},
28};
29
30pub struct SliderState {
38 pub is_dragging: bool,
40 pub focus: Focus,
42}
43
44impl Default for SliderState {
45 fn default() -> Self {
46 Self::new()
47 }
48}
49
50impl SliderState {
51 pub fn new() -> Self {
52 Self {
53 is_dragging: false,
54 focus: Focus::new(),
55 }
56 }
57}
58
59#[derive(Builder, Clone)]
61#[builder(pattern = "owned")]
62pub struct SliderArgs {
63 #[builder(default = "0.0")]
65 pub value: f32,
66
67 #[builder(default = "Arc::new(|_| {})")]
69 pub on_change: Arc<dyn Fn(f32) + Send + Sync>,
70
71 #[builder(default = "Dp(200.0)")]
73 pub width: Dp,
74
75 #[builder(default = "Dp(12.0)")]
77 pub track_height: Dp,
78
79 #[builder(default = "Color::new(0.2, 0.5, 0.8, 1.0)")]
81 pub active_track_color: Color,
82
83 #[builder(default = "Color::new(0.8, 0.8, 0.8, 1.0)")]
85 pub inactive_track_color: Color,
86
87 #[builder(default = "false")]
89 pub disabled: bool,
90}
91
92fn cursor_within_component(cursor_pos: Option<PxPosition>, computed: &ComputedData) -> bool {
94 if let Some(pos) = cursor_pos {
95 let within_x = pos.x.0 >= 0 && pos.x.0 < computed.width.0;
96 let within_y = pos.y.0 >= 0 && pos.y.0 < computed.height.0;
97 within_x && within_y
98 } else {
99 false
100 }
101}
102
103fn cursor_progress(cursor_pos: Option<PxPosition>, width_f: f32) -> Option<f32> {
106 cursor_pos.map(|pos| (pos.x.0 as f32 / width_f).clamp(0.0, 1.0))
107}
108
109fn handle_slider_state(
110 input: &mut InputHandlerInput,
111 state: &Arc<RwLock<SliderState>>,
112 args: &SliderArgs,
113) {
114 if args.disabled {
115 return;
116 }
117
118 let is_in_component = cursor_within_component(input.cursor_position_rel, &input.computed_data);
119
120 if is_in_component {
121 input.requests.cursor_icon = CursorIcon::Pointer;
122 }
123
124 if !is_in_component && !state.read().is_dragging {
125 return;
126 }
127
128 let width_f = input.computed_data.width.0 as f32;
129 let mut new_value: Option<f32> = None;
130
131 handle_cursor_events(input, &mut state.write(), &mut new_value, width_f);
132 update_value_on_drag(input, &state.read(), &mut new_value, width_f);
133 notify_on_change(new_value, args);
134}
135
136fn handle_cursor_events(
137 input: &mut InputHandlerInput,
138 state: &mut SliderState,
139 new_value: &mut Option<f32>,
140 width_f: f32,
141) {
142 for event in input.cursor_events.iter() {
143 match &event.content {
144 CursorEventContent::Pressed(_) => {
145 state.focus.request_focus();
146 state.is_dragging = true;
147 if let Some(v) = cursor_progress(input.cursor_position_rel, width_f) {
148 *new_value = Some(v);
149 }
150 }
151 CursorEventContent::Released(_) => {
152 state.is_dragging = false;
153 }
154 _ => {}
155 }
156 }
157}
158
159fn update_value_on_drag(
160 input: &InputHandlerInput,
161 state: &SliderState,
162 new_value: &mut Option<f32>,
163 width_f: f32,
164) {
165 if state.is_dragging
166 && let Some(v) = cursor_progress(input.cursor_position_rel, width_f)
167 {
168 *new_value = Some(v);
169 }
170}
171
172fn notify_on_change(new_value: Option<f32>, args: &SliderArgs) {
173 if let Some(v) = new_value
174 && (v - args.value).abs() > f32::EPSILON
175 {
176 (args.on_change)(v);
177 }
178}
179
180fn render_track(args: &SliderArgs) {
181 surface(
182 SurfaceArgsBuilder::default()
183 .width(DimensionValue::Fixed(args.width.to_px()))
184 .height(DimensionValue::Fixed(args.track_height.to_px()))
185 .style(args.inactive_track_color.into())
186 .shape({
187 let radius = Dp(args.track_height.0 / 2.0);
188 Shape::RoundedRectangle {
189 top_left: radius,
190 top_right: radius,
191 bottom_right: radius,
192 bottom_left: radius,
193 g2_k_value: 2.0, }
195 })
196 .build()
197 .unwrap(),
198 None,
199 move || {
200 render_progress_fill(args);
201 },
202 );
203}
204
205fn render_progress_fill(args: &SliderArgs) {
206 let progress_width = args.width.to_px().to_f32() * args.value;
207 surface(
208 SurfaceArgsBuilder::default()
209 .width(DimensionValue::Fixed(Px(progress_width as i32)))
210 .height(DimensionValue::Fill {
211 min: None,
212 max: None,
213 })
214 .style(args.active_track_color.into())
215 .shape({
216 let radius = Dp(args.track_height.0 / 2.0);
217 Shape::RoundedRectangle {
218 top_left: radius,
219 top_right: radius,
220 bottom_right: radius,
221 bottom_left: radius,
222 g2_k_value: 2.0, }
224 })
225 .build()
226 .unwrap(),
227 None,
228 || {},
229 );
230}
231
232fn measure_slider(
233 input: &MeasureInput,
234 args: &SliderArgs,
235) -> Result<ComputedData, MeasurementError> {
236 let self_width = args.width.to_px();
237 let self_height = args.track_height.to_px();
238
239 let track_id = input.children_ids[0];
240
241 let track_constraint = Constraint::new(
243 DimensionValue::Fixed(self_width),
244 DimensionValue::Fixed(self_height),
245 );
246 input.measure_child(track_id, &track_constraint)?;
247 input.place_child(track_id, PxPosition::new(Px(0), Px(0)));
248
249 Ok(ComputedData {
250 width: self_width,
251 height: self_height,
252 })
253}
254
255#[tessera]
304pub fn slider(args: impl Into<SliderArgs>, state: Arc<RwLock<SliderState>>) {
305 let args: SliderArgs = args.into();
306
307 render_track(&args);
308
309 let cloned_args = args.clone();
310 let state_clone = state.clone();
311 input_handler(Box::new(move |mut input| {
312 handle_slider_state(&mut input, &state_clone, &cloned_args);
313 }));
314
315 let cloned_args = args.clone();
316 measure(Box::new(move |input| measure_slider(input, &cloned_args)));
317}