tessera_ui_basic_components/
slider.rs1use std::sync::Arc;
7
8use derive_builder::Builder;
9use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
10use tessera_ui::{
11 Color, ComputedData, Constraint, CursorEventContent, DimensionValue, Dp, InputHandlerInput,
12 MeasureInput, MeasurementError, Px, PxPosition,
13 accesskit::{Action, Role},
14 focus_state::Focus,
15 tessera,
16 winit::window::CursorIcon,
17};
18
19use crate::{
20 shape_def::Shape,
21 surface::{SurfaceArgsBuilder, surface},
22};
23
24pub(crate) struct SliderStateInner {
27 pub is_dragging: bool,
29 pub focus: Focus,
31}
32
33impl Default for SliderStateInner {
34 fn default() -> Self {
35 Self::new()
36 }
37}
38
39impl SliderStateInner {
40 pub fn new() -> Self {
41 Self {
42 is_dragging: false,
43 focus: Focus::new(),
44 }
45 }
46}
47
48#[derive(Clone)]
49pub struct SliderState {
50 inner: Arc<RwLock<SliderStateInner>>,
51}
52
53impl SliderState {
54 pub fn new() -> Self {
55 Self {
56 inner: Arc::new(RwLock::new(SliderStateInner::new())),
57 }
58 }
59
60 pub(crate) fn read(&self) -> RwLockReadGuard<'_, SliderStateInner> {
61 self.inner.read()
62 }
63
64 pub(crate) fn write(&self) -> RwLockWriteGuard<'_, SliderStateInner> {
65 self.inner.write()
66 }
67
68 pub fn is_dragging(&self) -> bool {
70 self.inner.read().is_dragging
71 }
72
73 pub fn set_dragging(&self, dragging: bool) {
75 self.inner.write().is_dragging = dragging;
76 }
77
78 pub fn request_focus(&self) {
80 self.inner.write().focus.request_focus();
81 }
82
83 pub fn clear_focus(&self) {
85 self.inner.write().focus.unfocus();
86 }
87
88 pub fn is_focused(&self) -> bool {
90 self.inner.read().focus.is_focused()
91 }
92}
93
94impl Default for SliderState {
95 fn default() -> Self {
96 Self::new()
97 }
98}
99
100#[derive(Builder, Clone)]
102#[builder(pattern = "owned")]
103pub struct SliderArgs {
104 #[builder(default = "0.0")]
106 pub value: f32,
107
108 #[builder(default = "Arc::new(|_| {})")]
110 pub on_change: Arc<dyn Fn(f32) + Send + Sync>,
111
112 #[builder(default = "Dp(200.0)")]
114 pub width: Dp,
115
116 #[builder(default = "Dp(12.0)")]
118 pub track_height: Dp,
119
120 #[builder(default = "Color::new(0.2, 0.5, 0.8, 1.0)")]
122 pub active_track_color: Color,
123
124 #[builder(default = "Color::new(0.8, 0.8, 0.8, 1.0)")]
126 pub inactive_track_color: Color,
127
128 #[builder(default = "false")]
130 pub disabled: bool,
131 #[builder(default, setter(strip_option, into))]
133 pub accessibility_label: Option<String>,
134 #[builder(default, setter(strip_option, into))]
136 pub accessibility_description: Option<String>,
137}
138
139fn cursor_within_component(cursor_pos: Option<PxPosition>, computed: &ComputedData) -> bool {
141 if let Some(pos) = cursor_pos {
142 let within_x = pos.x.0 >= 0 && pos.x.0 < computed.width.0;
143 let within_y = pos.y.0 >= 0 && pos.y.0 < computed.height.0;
144 within_x && within_y
145 } else {
146 false
147 }
148}
149
150fn cursor_progress(cursor_pos: Option<PxPosition>, width_f: f32) -> Option<f32> {
153 cursor_pos.map(|pos| (pos.x.0 as f32 / width_f).clamp(0.0, 1.0))
154}
155
156fn handle_slider_state(input: &mut InputHandlerInput, state: &SliderState, args: &SliderArgs) {
157 if args.disabled {
158 return;
159 }
160
161 let is_in_component = cursor_within_component(input.cursor_position_rel, &input.computed_data);
162
163 if is_in_component {
164 input.requests.cursor_icon = CursorIcon::Pointer;
165 }
166
167 if !is_in_component && !state.read().is_dragging {
168 return;
169 }
170
171 let width_f = input.computed_data.width.0 as f32;
172 let mut new_value: Option<f32> = None;
173
174 handle_cursor_events(input, state, &mut new_value, width_f);
175 update_value_on_drag(input, state, &mut new_value, width_f);
176 notify_on_change(new_value, args);
177}
178
179fn handle_cursor_events(
180 input: &mut InputHandlerInput,
181 state: &SliderState,
182 new_value: &mut Option<f32>,
183 width_f: f32,
184) {
185 for event in input.cursor_events.iter() {
186 match &event.content {
187 CursorEventContent::Pressed(_) => {
188 {
189 let mut inner = state.write();
190 inner.focus.request_focus();
191 inner.is_dragging = true;
192 }
193 if let Some(v) = cursor_progress(input.cursor_position_rel, width_f) {
194 *new_value = Some(v);
195 }
196 }
197 CursorEventContent::Released(_) => {
198 state.write().is_dragging = false;
199 }
200 _ => {}
201 }
202 }
203}
204
205fn update_value_on_drag(
206 input: &InputHandlerInput,
207 state: &SliderState,
208 new_value: &mut Option<f32>,
209 width_f: f32,
210) {
211 if state.read().is_dragging
212 && let Some(v) = cursor_progress(input.cursor_position_rel, width_f)
213 {
214 *new_value = Some(v);
215 }
216}
217
218fn notify_on_change(new_value: Option<f32>, args: &SliderArgs) {
219 if let Some(v) = new_value
220 && (v - args.value).abs() > f32::EPSILON
221 {
222 (args.on_change)(v);
223 }
224}
225
226fn apply_slider_accessibility(
227 input: &mut InputHandlerInput<'_>,
228 args: &SliderArgs,
229 current_value: f32,
230 on_change: &Arc<dyn Fn(f32) + Send + Sync>,
231) {
232 let mut builder = input.accessibility().role(Role::Slider);
233
234 if let Some(label) = args.accessibility_label.as_ref() {
235 builder = builder.label(label.clone());
236 }
237 if let Some(description) = args.accessibility_description.as_ref() {
238 builder = builder.description(description.clone());
239 }
240
241 builder = builder
242 .numeric_value(current_value as f64)
243 .numeric_range(0.0, 1.0);
244
245 if args.disabled {
246 builder = builder.disabled();
247 } else {
248 builder = builder
249 .focusable()
250 .action(Action::Increment)
251 .action(Action::Decrement);
252 }
253
254 builder.commit();
255
256 if args.disabled {
257 return;
258 }
259
260 let value_for_handler = current_value;
261 let on_change = on_change.clone();
262 input.set_accessibility_action_handler(move |action| {
263 let new_value = match action {
264 Action::Increment => Some((value_for_handler + ACCESSIBILITY_STEP).clamp(0.0, 1.0)),
265 Action::Decrement => Some((value_for_handler - ACCESSIBILITY_STEP).clamp(0.0, 1.0)),
266 _ => None,
267 };
268
269 if let Some(new_value) = new_value
270 && (new_value - value_for_handler).abs() > f32::EPSILON
271 {
272 on_change(new_value);
273 }
274 });
275}
276
277fn render_track(args: &SliderArgs) {
278 surface(
279 SurfaceArgsBuilder::default()
280 .width(DimensionValue::Fixed(args.width.to_px()))
281 .height(DimensionValue::Fixed(args.track_height.to_px()))
282 .style(args.inactive_track_color.into())
283 .shape({
284 let radius = Dp(args.track_height.0 / 2.0);
285 Shape::RoundedRectangle {
286 top_left: radius,
287 top_right: radius,
288 bottom_right: radius,
289 bottom_left: radius,
290 g2_k_value: 2.0, }
292 })
293 .build()
294 .unwrap(),
295 None,
296 move || {
297 render_progress_fill(args);
298 },
299 );
300}
301
302fn render_progress_fill(args: &SliderArgs) {
303 let progress_width = args.width.to_px().to_f32() * args.value;
304 surface(
305 SurfaceArgsBuilder::default()
306 .width(DimensionValue::Fixed(Px(progress_width as i32)))
307 .height(DimensionValue::Fill {
308 min: None,
309 max: None,
310 })
311 .style(args.active_track_color.into())
312 .shape({
313 let radius = Dp(args.track_height.0 / 2.0);
314 Shape::RoundedRectangle {
315 top_left: radius,
316 top_right: radius,
317 bottom_right: radius,
318 bottom_left: radius,
319 g2_k_value: 2.0, }
321 })
322 .build()
323 .unwrap(),
324 None,
325 || {},
326 );
327}
328
329fn measure_slider(
330 input: &MeasureInput,
331 args: &SliderArgs,
332) -> Result<ComputedData, MeasurementError> {
333 let self_width = args.width.to_px();
334 let self_height = args.track_height.to_px();
335
336 let track_id = input.children_ids[0];
337
338 let track_constraint = Constraint::new(
340 DimensionValue::Fixed(self_width),
341 DimensionValue::Fixed(self_height),
342 );
343 input.measure_child(track_id, &track_constraint)?;
344 input.place_child(track_id, PxPosition::new(Px(0), Px(0)));
345
346 Ok(ComputedData {
347 width: self_width,
348 height: self_height,
349 })
350}
351
352#[tessera]
389pub fn slider(args: impl Into<SliderArgs>, state: SliderState) {
390 let args: SliderArgs = args.into();
391
392 render_track(&args);
393
394 let cloned_args = args.clone();
395 let state_clone = state.clone();
396 input_handler(Box::new(move |mut input| {
397 handle_slider_state(&mut input, &state_clone, &cloned_args);
398 apply_slider_accessibility(
399 &mut input,
400 &cloned_args,
401 cloned_args.value,
402 &cloned_args.on_change,
403 );
404 }));
405
406 let cloned_args = args.clone();
407 measure(Box::new(move |input| measure_slider(input, &cloned_args)));
408}
409const ACCESSIBILITY_STEP: f32 = 0.05;