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