1use std::sync::{Arc, LazyLock};
11
12use fret_core::{Axis, KeyCode, SemanticsRole};
13use fret_runtime::Model;
14use fret_ui::element::SemanticsProps;
15use fret_ui::{ElementContext, UiHost};
16
17use crate::primitives::direction::LayoutDirection;
18
19pub use crate::declarative::slider::{
20 start_slider_drag_from_pointer_axis, start_slider_drag_from_pointer_x,
21 update_single_slider_model_from_pointer_axis, update_single_slider_model_from_pointer_x,
22 update_slider_model_from_pointer_axis, update_slider_model_from_pointer_x,
23};
24pub use crate::headless::slider::{
25 SliderValuesUpdate, closest_value_index, format_semantics_value, has_min_steps_between_values,
26 next_sorted_values, normalize_value, snap_value, steps_between_values,
27 update_multi_thumb_values,
28};
29
30static THUMB_LABEL_MINIMUM: LazyLock<Arc<str>> = LazyLock::new(|| Arc::<str>::from("Minimum"));
31static THUMB_LABEL_MAXIMUM: LazyLock<Arc<str>> = LazyLock::new(|| Arc::<str>::from("Maximum"));
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub enum SliderOrientation {
35 #[default]
36 Horizontal,
37 Vertical,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum SliderSlideDirection {
42 FromLeft,
43 FromRight,
44 FromBottom,
45 FromTop,
46}
47
48pub fn slider_axis(orientation: SliderOrientation) -> Axis {
49 match orientation {
50 SliderOrientation::Horizontal => Axis::Horizontal,
51 SliderOrientation::Vertical => Axis::Vertical,
52 }
53}
54
55pub fn slider_slide_direction(
56 orientation: SliderOrientation,
57 dir: LayoutDirection,
58 inverted: bool,
59) -> SliderSlideDirection {
60 match orientation {
61 SliderOrientation::Horizontal => {
62 let is_direction_ltr = dir == LayoutDirection::Ltr;
63 let is_sliding_from_left =
64 (is_direction_ltr && !inverted) || (!is_direction_ltr && inverted);
65 if is_sliding_from_left {
66 SliderSlideDirection::FromLeft
67 } else {
68 SliderSlideDirection::FromRight
69 }
70 }
71 SliderOrientation::Vertical => {
72 let is_sliding_from_bottom = !inverted;
73 if is_sliding_from_bottom {
74 SliderSlideDirection::FromBottom
75 } else {
76 SliderSlideDirection::FromTop
77 }
78 }
79 }
80}
81
82pub fn slider_min_at_axis_start(
83 orientation: SliderOrientation,
84 dir: LayoutDirection,
85 inverted: bool,
86) -> bool {
87 match slider_slide_direction(orientation, dir, inverted) {
88 SliderSlideDirection::FromLeft | SliderSlideDirection::FromTop => true,
89 SliderSlideDirection::FromRight | SliderSlideDirection::FromBottom => false,
90 }
91}
92
93pub fn slider_position_t(
96 orientation: SliderOrientation,
97 dir: LayoutDirection,
98 inverted: bool,
99 value_t: f32,
100) -> f32 {
101 let pos_t = if slider_min_at_axis_start(orientation, dir, inverted) {
102 value_t
103 } else {
104 1.0 - value_t
105 };
106 pos_t.clamp(0.0, 1.0)
107}
108
109pub fn slider_is_back_key(slide_direction: SliderSlideDirection, key: KeyCode) -> bool {
110 match slide_direction {
113 SliderSlideDirection::FromLeft => matches!(
114 key,
115 KeyCode::Home | KeyCode::PageDown | KeyCode::ArrowDown | KeyCode::ArrowLeft
116 ),
117 SliderSlideDirection::FromRight => matches!(
118 key,
119 KeyCode::Home | KeyCode::PageDown | KeyCode::ArrowDown | KeyCode::ArrowRight
120 ),
121 SliderSlideDirection::FromBottom => matches!(
122 key,
123 KeyCode::Home | KeyCode::PageDown | KeyCode::ArrowDown | KeyCode::ArrowLeft
124 ),
125 SliderSlideDirection::FromTop => matches!(
126 key,
127 KeyCode::Home | KeyCode::PageDown | KeyCode::ArrowUp | KeyCode::ArrowLeft
128 ),
129 }
130}
131
132pub fn slider_step_direction_for_key(
133 slide_direction: SliderSlideDirection,
134 key: KeyCode,
135) -> Option<f32> {
136 match key {
137 KeyCode::PageUp
138 | KeyCode::PageDown
139 | KeyCode::ArrowUp
140 | KeyCode::ArrowDown
141 | KeyCode::ArrowLeft
142 | KeyCode::ArrowRight => Some(if slider_is_back_key(slide_direction, key) {
143 -1.0
144 } else {
145 1.0
146 }),
147 _ => None,
148 }
149}
150
151pub fn slider_use_values_model<H: UiHost>(
153 cx: &mut ElementContext<'_, H>,
154 controlled: Option<Model<Vec<f32>>>,
155 default_value: impl FnOnce() -> Vec<f32>,
156) -> crate::primitives::controllable_state::ControllableModel<Vec<f32>> {
157 crate::primitives::controllable_state::use_controllable_model(cx, controlled, default_value)
158}
159
160pub fn slider_root_semantics(
162 label: Option<Arc<str>>,
163 value: f32,
164 disabled: bool,
165) -> SemanticsProps {
166 SemanticsProps {
167 role: SemanticsRole::Generic,
168 label,
169 value: Some(format_semantics_value(value)),
170 numeric_value: value.is_finite().then_some(value as f64),
171 disabled,
172 ..Default::default()
173 }
174}
175
176pub fn slider_thumb_semantics(
178 label: Option<Arc<str>>,
179 value: f32,
180 disabled: bool,
181) -> SemanticsProps {
182 SemanticsProps {
183 role: SemanticsRole::Slider,
184 label,
185 value: Some(format_semantics_value(value)),
186 numeric_value: value.is_finite().then_some(value as f64),
187 disabled,
188 ..Default::default()
189 }
190}
191
192pub fn slider_thumb_default_label(index: usize, total_values: usize) -> Option<Arc<str>> {
197 if index >= total_values {
198 return None;
199 }
200
201 match total_values {
202 0 | 1 => None,
203 2 => match index {
204 0 => Some(THUMB_LABEL_MINIMUM.clone()),
205 1 => Some(THUMB_LABEL_MAXIMUM.clone()),
206 _ => None,
207 },
208 _ => Some(Arc::<str>::from(format!(
209 "Value {} of {}",
210 index.saturating_add(1),
211 total_values
212 ))),
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 use std::cell::Cell;
221 use std::sync::Arc;
222
223 use fret_app::App;
224 use fret_core::{AppWindowId, Point, Px, Rect, Size};
225
226 fn bounds() -> Rect {
227 Rect::new(
228 Point::new(Px(0.0), Px(0.0)),
229 Size::new(Px(200.0), Px(120.0)),
230 )
231 }
232
233 #[test]
234 fn slider_root_semantics_exposes_value_string() {
235 let label = Arc::<str>::from("slider");
236 let value = 12.0;
237
238 let out = slider_root_semantics(Some(label.clone()), value, false);
239 assert_eq!(out.label.as_deref(), Some(label.as_ref()));
240 assert_eq!(out.value, Some(format_semantics_value(value)));
241 assert_eq!(out.disabled, false);
242 }
243
244 #[test]
245 fn slider_use_values_model_prefers_controlled_and_does_not_call_default() {
246 let window = AppWindowId::default();
247 let mut app = App::new();
248 let b = bounds();
249
250 let controlled = app.models_mut().insert(vec![0.25]);
251 let called = Cell::new(0);
252
253 fret_ui::elements::with_element_cx(&mut app, window, b, "test", |cx| {
254 let out = slider_use_values_model(cx, Some(controlled.clone()), || {
255 called.set(called.get() + 1);
256 vec![0.0]
257 });
258 assert!(out.is_controlled());
259 assert_eq!(out.model(), controlled);
260 });
261
262 assert_eq!(called.get(), 0);
263 }
264
265 #[test]
266 fn slider_slide_direction_matches_radix_rules() {
267 assert_eq!(
268 slider_slide_direction(SliderOrientation::Horizontal, LayoutDirection::Ltr, false),
269 SliderSlideDirection::FromLeft
270 );
271 assert_eq!(
272 slider_slide_direction(SliderOrientation::Horizontal, LayoutDirection::Rtl, false),
273 SliderSlideDirection::FromRight
274 );
275 assert_eq!(
276 slider_slide_direction(SliderOrientation::Horizontal, LayoutDirection::Ltr, true),
277 SliderSlideDirection::FromRight
278 );
279 assert_eq!(
280 slider_slide_direction(SliderOrientation::Horizontal, LayoutDirection::Rtl, true),
281 SliderSlideDirection::FromLeft
282 );
283
284 assert_eq!(
285 slider_slide_direction(SliderOrientation::Vertical, LayoutDirection::Ltr, false),
286 SliderSlideDirection::FromBottom
287 );
288 assert_eq!(
289 slider_slide_direction(SliderOrientation::Vertical, LayoutDirection::Rtl, false),
290 SliderSlideDirection::FromBottom
291 );
292 assert_eq!(
293 slider_slide_direction(SliderOrientation::Vertical, LayoutDirection::Ltr, true),
294 SliderSlideDirection::FromTop
295 );
296 }
297
298 #[test]
299 fn slider_step_direction_for_key_uses_back_key_table() {
300 assert_eq!(
301 slider_step_direction_for_key(SliderSlideDirection::FromLeft, KeyCode::ArrowLeft),
302 Some(-1.0)
303 );
304 assert_eq!(
305 slider_step_direction_for_key(SliderSlideDirection::FromLeft, KeyCode::ArrowRight),
306 Some(1.0)
307 );
308 assert_eq!(
309 slider_step_direction_for_key(SliderSlideDirection::FromRight, KeyCode::ArrowLeft),
310 Some(1.0)
311 );
312 assert_eq!(
313 slider_step_direction_for_key(SliderSlideDirection::FromRight, KeyCode::ArrowRight),
314 Some(-1.0)
315 );
316 assert_eq!(
317 slider_step_direction_for_key(SliderSlideDirection::FromTop, KeyCode::ArrowUp),
318 Some(-1.0)
319 );
320 assert_eq!(
321 slider_step_direction_for_key(SliderSlideDirection::FromBottom, KeyCode::ArrowDown),
322 Some(-1.0)
323 );
324 }
325
326 #[test]
327 fn slider_thumb_default_label_matches_radix_get_label() {
328 assert_eq!(slider_thumb_default_label(0, 1), None);
329 assert_eq!(slider_thumb_default_label(0, 2).as_deref(), Some("Minimum"));
330 assert_eq!(slider_thumb_default_label(1, 2).as_deref(), Some("Maximum"));
331 assert_eq!(
332 slider_thumb_default_label(0, 3).as_deref(),
333 Some("Value 1 of 3")
334 );
335 assert_eq!(
336 slider_thumb_default_label(2, 3).as_deref(),
337 Some("Value 3 of 3")
338 );
339 assert_eq!(slider_thumb_default_label(3, 3), None);
340 }
341}