1#![allow(non_snake_case)]
4
5use std::{cell::RefCell, rc::Rc};
6
7use cranpose_core::{NodeId, State, rememberMutableStateOf, rememberUpdatedState};
8use cranpose_foundation::{PointerEventKind, PointerId};
9
10use crate::{
11 Modifier, MutableInteractionSource, composable,
12 widgets::{Box, BoxSpec, BoxWithConstraints, scopes::BoxWithConstraintsScope},
13};
14
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
17pub enum SliderOrientation {
18 #[default]
20 Horizontal,
21 Vertical,
23}
24
25#[derive(Clone, Copy, Debug, PartialEq)]
27pub struct SliderSpec {
28 pub orientation: SliderOrientation,
29 pub reverse_direction: bool,
30 pub thumb_extent: f32,
31 pub enabled: bool,
32 pub rotary_step: f32,
33}
34
35impl SliderSpec {
36 pub fn new() -> Self {
37 Self::default()
38 }
39
40 pub fn orientation(mut self, orientation: SliderOrientation) -> Self {
41 self.orientation = orientation;
42 self
43 }
44
45 pub fn reverse_direction(mut self, reverse_direction: bool) -> Self {
46 self.reverse_direction = reverse_direction;
47 self
48 }
49
50 pub fn thumb_extent(mut self, thumb_extent: f32) -> Self {
51 self.thumb_extent = thumb_extent.max(0.0);
52 self
53 }
54
55 pub fn enabled(mut self, enabled: bool) -> Self {
56 self.enabled = enabled;
57 self
58 }
59
60 pub fn rotary_step(mut self, rotary_step: f32) -> Self {
61 self.rotary_step = rotary_step.abs();
62 self
63 }
64}
65
66impl Default for SliderSpec {
67 fn default() -> Self {
68 Self {
69 orientation: SliderOrientation::Horizontal,
70 reverse_direction: false,
71 thumb_extent: 0.0,
72 enabled: true,
73 rotary_step: 0.05,
74 }
75 }
76}
77
78#[derive(Clone)]
80pub struct SliderScope {
81 value: f32,
82 track_extent: f32,
83 thumb_offset: f32,
84 dragging: State<bool>,
85 interaction_source: MutableInteractionSource,
86}
87
88impl SliderScope {
89 pub fn value(&self) -> f32 {
91 self.value
92 }
93
94 pub fn track_extent(&self) -> f32 {
96 self.track_extent
97 }
98
99 pub fn thumb_offset(&self) -> f32 {
101 self.thumb_offset
102 }
103
104 pub fn is_dragging(&self) -> bool {
106 self.dragging.get()
107 }
108
109 pub fn interaction_source(&self) -> MutableInteractionSource {
111 self.interaction_source
112 }
113}
114
115#[composable]
119pub fn Slider<F>(
120 modifier: Modifier,
121 value: f32,
122 on_value_change: impl Fn(f32) + 'static,
123 on_value_change_finished: impl Fn() + 'static,
124 spec: SliderSpec,
125 content: F,
126) -> NodeId
127where
128 F: FnMut(SliderScope) + 'static,
129{
130 let value = value.clamp(0.0, 1.0);
131 let current_value = rememberUpdatedState(value);
132 let on_value_change: Rc<dyn Fn(f32)> = Rc::new(on_value_change);
133 let on_value_change = rememberUpdatedState(on_value_change);
134 let on_value_change_finished: Rc<dyn Fn()> = Rc::new(on_value_change_finished);
135 let on_value_change_finished = rememberUpdatedState(on_value_change_finished);
136 let dragging = rememberMutableStateOf(|| false);
137 let interaction_source = crate::rememberMutableInteractionSource();
138 let content = Rc::new(RefCell::new(content));
139
140 BoxWithConstraints(modifier, move |constraints_scope| {
141 let constraints = constraints_scope.constraints();
142 let extent = match spec.orientation {
143 SliderOrientation::Horizontal => constraints.max_width,
144 SliderOrientation::Vertical => constraints.max_height,
145 }
146 .max(0.0);
147 let track_extent = (extent - spec.thumb_extent).max(0.0);
148 let logical_value = if spec.reverse_direction {
149 1.0 - value
150 } else {
151 value
152 };
153 let slider_scope = SliderScope {
154 value,
155 track_extent,
156 thumb_offset: track_extent * logical_value,
157 dragging: dragging.as_state(),
158 interaction_source,
159 };
160 let interaction = interaction_source;
161 let content = Rc::clone(&content);
162 let input = Modifier::empty()
163 .fill_max_size()
164 .semantics(move |config| {
165 slider_semantics(
166 config,
167 value,
168 spec.enabled,
169 on_value_change,
170 on_value_change_finished,
171 );
172 })
173 .pointer_input(
174 (
175 spec.orientation,
176 spec.reverse_direction,
177 spec.thumb_extent.to_bits(),
178 spec.enabled,
179 spec.rotary_step.to_bits(),
180 extent.to_bits(),
181 ),
182 move |pointer_scope| {
183 let interaction = interaction;
184 async move {
185 pointer_scope
186 .await_pointer_event_scope(|await_scope| async move {
187 let mut active_pointer: Option<PointerId> = None;
188 let mut active_press = None;
189 loop {
190 let event = await_scope.await_pointer_event().await;
191 match event.kind {
192 PointerEventKind::Down
193 if spec.enabled && active_pointer.is_none() =>
194 {
195 active_pointer = Some(event.id);
196 dragging.set(true);
197 active_press = Some(interaction.press(event.position));
198 let next = value_for_position(
199 axis_position(
200 event.position.x,
201 event.position.y,
202 spec,
203 ),
204 extent,
205 spec.thumb_extent,
206 spec.reverse_direction,
207 );
208 (on_value_change.value())(next);
209 event.consume();
210 }
211 PointerEventKind::Move
212 if active_pointer == Some(event.id) =>
213 {
214 let next = value_for_position(
215 axis_position(
216 event.position.x,
217 event.position.y,
218 spec,
219 ),
220 extent,
221 spec.thumb_extent,
222 spec.reverse_direction,
223 );
224 (on_value_change.value())(next);
225 event.consume();
226 }
227 PointerEventKind::Up
228 if active_pointer == Some(event.id) =>
229 {
230 let next = value_for_position(
231 axis_position(
232 event.position.x,
233 event.position.y,
234 spec,
235 ),
236 extent,
237 spec.thumb_extent,
238 spec.reverse_direction,
239 );
240 (on_value_change.value())(next);
241 if let Some(press) = active_press.take() {
242 interaction.release(press);
243 }
244 dragging.set(false);
245 active_pointer = None;
246 (on_value_change_finished.value())();
247 event.consume();
248 }
249 PointerEventKind::Cancel
250 if active_pointer == Some(event.id) =>
251 {
252 if let Some(press) = active_press.take() {
253 interaction.cancel(press);
254 }
255 dragging.set(false);
256 active_pointer = None;
257 (on_value_change_finished.value())();
258 event.consume();
259 }
260 PointerEventKind::RotaryScroll if spec.enabled => {
261 let delta = event.scroll_delta.y;
262 if delta != 0.0 {
263 let direction =
264 if spec.reverse_direction { 1.0 } else { -1.0 };
265 let next = (current_value.value()
266 + direction
267 * delta.signum()
268 * spec.rotary_step)
269 .clamp(0.0, 1.0);
270 (on_value_change.value())(next);
271 (on_value_change_finished.value())();
272 event.consume();
273 }
274 }
275 _ => {}
276 }
277 }
278 })
279 .await;
280 }
281 },
282 );
283 Box(input, BoxSpec::default(), move || {
284 (content.borrow_mut())(slider_scope.clone())
285 });
286 })
287}
288
289fn axis_position(x: f32, y: f32, spec: SliderSpec) -> f32 {
290 match spec.orientation {
291 SliderOrientation::Horizontal => x,
292 SliderOrientation::Vertical => y,
293 }
294}
295
296fn value_for_position(position: f32, extent: f32, thumb_extent: f32, reverse: bool) -> f32 {
297 let travel = (extent - thumb_extent).max(0.0);
298 let mut value = if travel > 0.0 {
299 ((position - thumb_extent * 0.5) / travel).clamp(0.0, 1.0)
300 } else {
301 0.0
302 };
303 if reverse {
304 value = 1.0 - value;
305 }
306 value
307}
308
309fn slider_semantics(
313 config: &mut cranpose_foundation::SemanticsConfiguration,
314 value: f32,
315 enabled: bool,
316 on_value_change: cranpose_core::MutableState<Rc<dyn Fn(f32)>>,
317 on_value_change_finished: cranpose_core::MutableState<Rc<dyn Fn()>>,
318) {
319 config.enabled = enabled;
320 config.state_description = Some(format!("{}%", (value * 100.0).round() as u32));
321 config.progress = Some(cranpose_foundation::ProgressBarRangeInfo::new(
322 value, 0.0, 1.0, 0,
323 ));
324 if enabled {
325 config.set_progress = Some(cranpose_foundation::SemanticsSetProgress::new(
326 move |next: f32| {
327 (on_value_change.value())(next.clamp(0.0, 1.0));
328 (on_value_change_finished.value())();
329 true
330 },
331 ));
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn spec_builders_define_orientation_and_input_policy() {
341 let spec = SliderSpec::new()
342 .orientation(SliderOrientation::Vertical)
343 .reverse_direction(true)
344 .thumb_extent(11.0)
345 .enabled(false)
346 .rotary_step(-0.2);
347 assert_eq!(spec.orientation, SliderOrientation::Vertical);
348 assert!(spec.reverse_direction);
349 assert_eq!(spec.thumb_extent, 11.0);
350 assert!(!spec.enabled);
351 assert_eq!(spec.rotary_step, 0.2);
352 }
353
354 #[test]
355 fn pointer_position_tracks_thumb_centre_and_reverse_direction() {
356 assert_eq!(value_for_position(5.0, 110.0, 10.0, false), 0.0);
357 assert_eq!(value_for_position(105.0, 110.0, 10.0, false), 1.0);
358 assert_eq!(value_for_position(55.0, 110.0, 10.0, false), 0.5);
359 assert_eq!(value_for_position(5.0, 110.0, 10.0, true), 1.0);
360 }
361
362 #[test]
363 fn zero_travel_is_stable() {
364 assert_eq!(value_for_position(0.0, 0.0, 0.0, false), 0.0);
365 assert_eq!(value_for_position(0.0, 0.0, 0.0, true), 1.0);
366 }
367}