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 config.enabled = spec.enabled;
166 config.state_description = Some(format!("{}%", (value * 100.0).round() as u32));
167 })
168 .pointer_input(
169 (
170 spec.orientation,
171 spec.reverse_direction,
172 spec.thumb_extent.to_bits(),
173 spec.enabled,
174 spec.rotary_step.to_bits(),
175 extent.to_bits(),
176 ),
177 move |pointer_scope| {
178 let interaction = interaction;
179 async move {
180 pointer_scope
181 .await_pointer_event_scope(|await_scope| async move {
182 let mut active_pointer: Option<PointerId> = None;
183 let mut active_press = None;
184 loop {
185 let event = await_scope.await_pointer_event().await;
186 match event.kind {
187 PointerEventKind::Down
188 if spec.enabled && active_pointer.is_none() =>
189 {
190 active_pointer = Some(event.id);
191 dragging.set(true);
192 active_press = Some(interaction.press(event.position));
193 let next = value_for_position(
194 axis_position(
195 event.position.x,
196 event.position.y,
197 spec,
198 ),
199 extent,
200 spec.thumb_extent,
201 spec.reverse_direction,
202 );
203 (on_value_change.value())(next);
204 event.consume();
205 }
206 PointerEventKind::Move
207 if active_pointer == Some(event.id) =>
208 {
209 let next = value_for_position(
210 axis_position(
211 event.position.x,
212 event.position.y,
213 spec,
214 ),
215 extent,
216 spec.thumb_extent,
217 spec.reverse_direction,
218 );
219 (on_value_change.value())(next);
220 event.consume();
221 }
222 PointerEventKind::Up
223 if active_pointer == Some(event.id) =>
224 {
225 let next = value_for_position(
226 axis_position(
227 event.position.x,
228 event.position.y,
229 spec,
230 ),
231 extent,
232 spec.thumb_extent,
233 spec.reverse_direction,
234 );
235 (on_value_change.value())(next);
236 if let Some(press) = active_press.take() {
237 interaction.release(press);
238 }
239 dragging.set(false);
240 active_pointer = None;
241 (on_value_change_finished.value())();
242 event.consume();
243 }
244 PointerEventKind::Cancel
245 if active_pointer == Some(event.id) =>
246 {
247 if let Some(press) = active_press.take() {
248 interaction.cancel(press);
249 }
250 dragging.set(false);
251 active_pointer = None;
252 (on_value_change_finished.value())();
253 event.consume();
254 }
255 PointerEventKind::RotaryScroll if spec.enabled => {
256 let delta = event.scroll_delta.y;
257 if delta != 0.0 {
258 let direction =
259 if spec.reverse_direction { 1.0 } else { -1.0 };
260 let next = (current_value.value()
261 + direction
262 * delta.signum()
263 * spec.rotary_step)
264 .clamp(0.0, 1.0);
265 (on_value_change.value())(next);
266 (on_value_change_finished.value())();
267 event.consume();
268 }
269 }
270 _ => {}
271 }
272 }
273 })
274 .await;
275 }
276 },
277 );
278 Box(input, BoxSpec::default(), move || {
279 (content.borrow_mut())(slider_scope.clone())
280 });
281 })
282}
283
284fn axis_position(x: f32, y: f32, spec: SliderSpec) -> f32 {
285 match spec.orientation {
286 SliderOrientation::Horizontal => x,
287 SliderOrientation::Vertical => y,
288 }
289}
290
291fn value_for_position(position: f32, extent: f32, thumb_extent: f32, reverse: bool) -> f32 {
292 let travel = (extent - thumb_extent).max(0.0);
293 let mut value = if travel > 0.0 {
294 ((position - thumb_extent * 0.5) / travel).clamp(0.0, 1.0)
295 } else {
296 0.0
297 };
298 if reverse {
299 value = 1.0 - value;
300 }
301 value
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn spec_builders_define_orientation_and_input_policy() {
310 let spec = SliderSpec::new()
311 .orientation(SliderOrientation::Vertical)
312 .reverse_direction(true)
313 .thumb_extent(11.0)
314 .enabled(false)
315 .rotary_step(-0.2);
316 assert_eq!(spec.orientation, SliderOrientation::Vertical);
317 assert!(spec.reverse_direction);
318 assert_eq!(spec.thumb_extent, 11.0);
319 assert!(!spec.enabled);
320 assert_eq!(spec.rotary_step, 0.2);
321 }
322
323 #[test]
324 fn pointer_position_tracks_thumb_centre_and_reverse_direction() {
325 assert_eq!(value_for_position(5.0, 110.0, 10.0, false), 0.0);
326 assert_eq!(value_for_position(105.0, 110.0, 10.0, false), 1.0);
327 assert_eq!(value_for_position(55.0, 110.0, 10.0, false), 0.5);
328 assert_eq!(value_for_position(5.0, 110.0, 10.0, true), 1.0);
329 }
330
331 #[test]
332 fn zero_travel_is_stable() {
333 assert_eq!(value_for_position(0.0, 0.0, 0.0, false), 0.0);
334 assert_eq!(value_for_position(0.0, 0.0, 0.0, true), 1.0);
335 }
336}