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