egui/widgets/drag_value.rs
1#![allow(clippy::needless_pass_by_value)] // False positives with `impl ToString`
2
3use std::{cmp::Ordering, ops::RangeInclusive};
4
5use crate::*;
6
7// ----------------------------------------------------------------------------
8
9type NumFormatter<'a> = Box<dyn 'a + Fn(f64, RangeInclusive<usize>) -> String>;
10type NumParser<'a> = Box<dyn 'a + Fn(&str) -> Option<f64>>;
11
12// ----------------------------------------------------------------------------
13
14/// Combined into one function (rather than two) to make it easier
15/// for the borrow checker.
16type GetSetValue<'a> = Box<dyn 'a + FnMut(Option<f64>) -> f64>;
17
18fn get(get_set_value: &mut GetSetValue<'_>) -> f64 {
19 (get_set_value)(None)
20}
21
22fn set(get_set_value: &mut GetSetValue<'_>, value: f64) {
23 (get_set_value)(Some(value));
24}
25
26/// A numeric value that you can change by dragging the number. More compact than a [`Slider`].
27///
28/// ```
29/// # egui::__run_test_ui(|ui| {
30/// # let mut my_f32: f32 = 0.0;
31/// ui.add(egui::DragValue::new(&mut my_f32).speed(0.1));
32/// # });
33/// ```
34#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
35pub struct DragValue<'a> {
36 get_set_value: GetSetValue<'a>,
37 speed: f64,
38 prefix: String,
39 suffix: String,
40 range: RangeInclusive<f64>,
41 clamp_to_range: bool,
42 min_decimals: usize,
43 max_decimals: Option<usize>,
44 custom_formatter: Option<NumFormatter<'a>>,
45 custom_parser: Option<NumParser<'a>>,
46 update_while_editing: bool,
47}
48
49impl<'a> DragValue<'a> {
50 pub fn new<Num: emath::Numeric>(value: &'a mut Num) -> Self {
51 let slf = Self::from_get_set(move |v: Option<f64>| {
52 if let Some(v) = v {
53 *value = Num::from_f64(v);
54 }
55 value.to_f64()
56 });
57
58 if Num::INTEGRAL {
59 slf.max_decimals(0).range(Num::MIN..=Num::MAX).speed(0.25)
60 } else {
61 slf
62 }
63 }
64
65 pub fn from_get_set(get_set_value: impl 'a + FnMut(Option<f64>) -> f64) -> Self {
66 Self {
67 get_set_value: Box::new(get_set_value),
68 speed: 1.0,
69 prefix: Default::default(),
70 suffix: Default::default(),
71 range: f64::NEG_INFINITY..=f64::INFINITY,
72 clamp_to_range: true,
73 min_decimals: 0,
74 max_decimals: None,
75 custom_formatter: None,
76 custom_parser: None,
77 update_while_editing: true,
78 }
79 }
80
81 /// How much the value changes when dragged one point (logical pixel).
82 ///
83 /// Should be finite and greater than zero.
84 #[inline]
85 pub fn speed(mut self, speed: impl Into<f64>) -> Self {
86 self.speed = speed.into();
87 self
88 }
89
90 /// Sets valid range for the value.
91 ///
92 /// By default all values are clamped to this range, even when not interacted with.
93 /// You can change this behavior by passing `false` to [`Slider::clamp_to_range`].
94 #[deprecated = "Use `range` instead"]
95 #[inline]
96 pub fn clamp_range<Num: emath::Numeric>(mut self, range: RangeInclusive<Num>) -> Self {
97 self.range = range.start().to_f64()..=range.end().to_f64();
98 self
99 }
100
101 /// Sets valid range for dragging the value.
102 ///
103 /// By default all values are clamped to this range, even when not interacted with.
104 /// You can change this behavior by passing `false` to [`Slider::clamp_to_range`].
105 #[inline]
106 pub fn range<Num: emath::Numeric>(mut self, range: RangeInclusive<Num>) -> Self {
107 self.range = range.start().to_f64()..=range.end().to_f64();
108 self
109 }
110
111 /// If set to `true`, all incoming and outgoing values will be clamped to the sliding [`Self::range`] (if any).
112 ///
113 /// If set to `false`, a value outside of the range that is set programmatically or by user input will not be changed.
114 /// Dragging will be restricted to the range regardless of this setting.
115 /// Default: `true`.
116 #[inline]
117 pub fn clamp_to_range(mut self, clamp_to_range: bool) -> Self {
118 self.clamp_to_range = clamp_to_range;
119 self
120 }
121
122 /// Show a prefix before the number, e.g. "x: "
123 #[inline]
124 pub fn prefix(mut self, prefix: impl ToString) -> Self {
125 self.prefix = prefix.to_string();
126 self
127 }
128
129 /// Add a suffix to the number, this can be e.g. a unit ("°" or " m")
130 #[inline]
131 pub fn suffix(mut self, suffix: impl ToString) -> Self {
132 self.suffix = suffix.to_string();
133 self
134 }
135
136 // TODO(emilk): we should also have a "min precision".
137 /// Set a minimum number of decimals to display.
138 /// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
139 /// Regardless of precision the slider will use "smart aim" to help the user select nice, round values.
140 #[inline]
141 pub fn min_decimals(mut self, min_decimals: usize) -> Self {
142 self.min_decimals = min_decimals;
143 self
144 }
145
146 // TODO(emilk): we should also have a "max precision".
147 /// Set a maximum number of decimals to display.
148 /// Values will also be rounded to this number of decimals.
149 /// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
150 /// Regardless of precision the slider will use "smart aim" to help the user select nice, round values.
151 #[inline]
152 pub fn max_decimals(mut self, max_decimals: usize) -> Self {
153 self.max_decimals = Some(max_decimals);
154 self
155 }
156
157 #[inline]
158 pub fn max_decimals_opt(mut self, max_decimals: Option<usize>) -> Self {
159 self.max_decimals = max_decimals;
160 self
161 }
162
163 /// Set an exact number of decimals to display.
164 /// Values will also be rounded to this number of decimals.
165 /// Normally you don't need to pick a precision, as the slider will intelligently pick a precision for you.
166 /// Regardless of precision the slider will use "smart aim" to help the user select nice, round values.
167 #[inline]
168 pub fn fixed_decimals(mut self, num_decimals: usize) -> Self {
169 self.min_decimals = num_decimals;
170 self.max_decimals = Some(num_decimals);
171 self
172 }
173
174 /// Set custom formatter defining how numbers are converted into text.
175 ///
176 /// A custom formatter takes a `f64` for the numeric value and a `RangeInclusive<usize>` representing
177 /// the decimal range i.e. minimum and maximum number of decimal places shown.
178 ///
179 /// The default formatter is [`Style::number_formatter`].
180 ///
181 /// See also: [`DragValue::custom_parser`]
182 ///
183 /// ```
184 /// # egui::__run_test_ui(|ui| {
185 /// # let mut my_i32: i32 = 0;
186 /// ui.add(egui::DragValue::new(&mut my_i32)
187 /// .range(0..=((60 * 60 * 24) - 1))
188 /// .custom_formatter(|n, _| {
189 /// let n = n as i32;
190 /// let hours = n / (60 * 60);
191 /// let mins = (n / 60) % 60;
192 /// let secs = n % 60;
193 /// format!("{hours:02}:{mins:02}:{secs:02}")
194 /// })
195 /// .custom_parser(|s| {
196 /// let parts: Vec<&str> = s.split(':').collect();
197 /// if parts.len() == 3 {
198 /// parts[0].parse::<i32>().and_then(|h| {
199 /// parts[1].parse::<i32>().and_then(|m| {
200 /// parts[2].parse::<i32>().map(|s| {
201 /// ((h * 60 * 60) + (m * 60) + s) as f64
202 /// })
203 /// })
204 /// })
205 /// .ok()
206 /// } else {
207 /// None
208 /// }
209 /// }));
210 /// # });
211 /// ```
212 pub fn custom_formatter(
213 mut self,
214 formatter: impl 'a + Fn(f64, RangeInclusive<usize>) -> String,
215 ) -> Self {
216 self.custom_formatter = Some(Box::new(formatter));
217 self
218 }
219
220 /// Set custom parser defining how the text input is parsed into a number.
221 ///
222 /// A custom parser takes an `&str` to parse into a number and returns a `f64` if it was successfully parsed
223 /// or `None` otherwise.
224 ///
225 /// See also: [`DragValue::custom_formatter`]
226 ///
227 /// ```
228 /// # egui::__run_test_ui(|ui| {
229 /// # let mut my_i32: i32 = 0;
230 /// ui.add(egui::DragValue::new(&mut my_i32)
231 /// .range(0..=((60 * 60 * 24) - 1))
232 /// .custom_formatter(|n, _| {
233 /// let n = n as i32;
234 /// let hours = n / (60 * 60);
235 /// let mins = (n / 60) % 60;
236 /// let secs = n % 60;
237 /// format!("{hours:02}:{mins:02}:{secs:02}")
238 /// })
239 /// .custom_parser(|s| {
240 /// let parts: Vec<&str> = s.split(':').collect();
241 /// if parts.len() == 3 {
242 /// parts[0].parse::<i32>().and_then(|h| {
243 /// parts[1].parse::<i32>().and_then(|m| {
244 /// parts[2].parse::<i32>().map(|s| {
245 /// ((h * 60 * 60) + (m * 60) + s) as f64
246 /// })
247 /// })
248 /// })
249 /// .ok()
250 /// } else {
251 /// None
252 /// }
253 /// }));
254 /// # });
255 /// ```
256 #[inline]
257 pub fn custom_parser(mut self, parser: impl 'a + Fn(&str) -> Option<f64>) -> Self {
258 self.custom_parser = Some(Box::new(parser));
259 self
260 }
261
262 /// Set `custom_formatter` and `custom_parser` to display and parse numbers as binary integers. Floating point
263 /// numbers are *not* supported.
264 ///
265 /// `min_width` specifies the minimum number of displayed digits; if the number is shorter than this, it will be
266 /// prefixed with additional 0s to match `min_width`.
267 ///
268 /// If `twos_complement` is true, negative values will be displayed as the 2's complement representation. Otherwise
269 /// they will be prefixed with a '-' sign.
270 ///
271 /// # Panics
272 ///
273 /// Panics if `min_width` is 0.
274 ///
275 /// ```
276 /// # egui::__run_test_ui(|ui| {
277 /// # let mut my_i32: i32 = 0;
278 /// ui.add(egui::DragValue::new(&mut my_i32).binary(64, false));
279 /// # });
280 /// ```
281 pub fn binary(self, min_width: usize, twos_complement: bool) -> Self {
282 assert!(
283 min_width > 0,
284 "DragValue::binary: `min_width` must be greater than 0"
285 );
286 if twos_complement {
287 self.custom_formatter(move |n, _| format!("{:0>min_width$b}", n as i64))
288 } else {
289 self.custom_formatter(move |n, _| {
290 let sign = if n < 0.0 { MINUS_CHAR_STR } else { "" };
291 format!("{sign}{:0>min_width$b}", n.abs() as i64)
292 })
293 }
294 .custom_parser(|s| i64::from_str_radix(s, 2).map(|n| n as f64).ok())
295 }
296
297 /// Set `custom_formatter` and `custom_parser` to display and parse numbers as octal integers. Floating point
298 /// numbers are *not* supported.
299 ///
300 /// `min_width` specifies the minimum number of displayed digits; if the number is shorter than this, it will be
301 /// prefixed with additional 0s to match `min_width`.
302 ///
303 /// If `twos_complement` is true, negative values will be displayed as the 2's complement representation. Otherwise
304 /// they will be prefixed with a '-' sign.
305 ///
306 /// # Panics
307 ///
308 /// Panics if `min_width` is 0.
309 ///
310 /// ```
311 /// # egui::__run_test_ui(|ui| {
312 /// # let mut my_i32: i32 = 0;
313 /// ui.add(egui::DragValue::new(&mut my_i32).octal(22, false));
314 /// # });
315 /// ```
316 pub fn octal(self, min_width: usize, twos_complement: bool) -> Self {
317 assert!(
318 min_width > 0,
319 "DragValue::octal: `min_width` must be greater than 0"
320 );
321 if twos_complement {
322 self.custom_formatter(move |n, _| format!("{:0>min_width$o}", n as i64))
323 } else {
324 self.custom_formatter(move |n, _| {
325 let sign = if n < 0.0 { MINUS_CHAR_STR } else { "" };
326 format!("{sign}{:0>min_width$o}", n.abs() as i64)
327 })
328 }
329 .custom_parser(|s| i64::from_str_radix(s, 8).map(|n| n as f64).ok())
330 }
331
332 /// Set `custom_formatter` and `custom_parser` to display and parse numbers as hexadecimal integers. Floating point
333 /// numbers are *not* supported.
334 ///
335 /// `min_width` specifies the minimum number of displayed digits; if the number is shorter than this, it will be
336 /// prefixed with additional 0s to match `min_width`.
337 ///
338 /// If `twos_complement` is true, negative values will be displayed as the 2's complement representation. Otherwise
339 /// they will be prefixed with a '-' sign.
340 ///
341 /// # Panics
342 ///
343 /// Panics if `min_width` is 0.
344 ///
345 /// ```
346 /// # egui::__run_test_ui(|ui| {
347 /// # let mut my_i32: i32 = 0;
348 /// ui.add(egui::DragValue::new(&mut my_i32).hexadecimal(16, false, true));
349 /// # });
350 /// ```
351 pub fn hexadecimal(self, min_width: usize, twos_complement: bool, upper: bool) -> Self {
352 assert!(
353 min_width > 0,
354 "DragValue::hexadecimal: `min_width` must be greater than 0"
355 );
356 match (twos_complement, upper) {
357 (true, true) => {
358 self.custom_formatter(move |n, _| format!("{:0>min_width$X}", n as i64))
359 }
360 (true, false) => {
361 self.custom_formatter(move |n, _| format!("{:0>min_width$x}", n as i64))
362 }
363 (false, true) => self.custom_formatter(move |n, _| {
364 let sign = if n < 0.0 { MINUS_CHAR_STR } else { "" };
365 format!("{sign}{:0>min_width$X}", n.abs() as i64)
366 }),
367 (false, false) => self.custom_formatter(move |n, _| {
368 let sign = if n < 0.0 { MINUS_CHAR_STR } else { "" };
369 format!("{sign}{:0>min_width$x}", n.abs() as i64)
370 }),
371 }
372 .custom_parser(|s| i64::from_str_radix(s, 16).map(|n| n as f64).ok())
373 }
374
375 /// Update the value on each key press when text-editing the value.
376 ///
377 /// Default: `true`.
378 /// If `false`, the value will only be updated when user presses enter or deselects the value.
379 #[inline]
380 pub fn update_while_editing(mut self, update: bool) -> Self {
381 self.update_while_editing = update;
382 self
383 }
384}
385
386impl<'a> Widget for DragValue<'a> {
387 fn ui(self, ui: &mut Ui) -> Response {
388 let Self {
389 mut get_set_value,
390 speed,
391 range,
392 clamp_to_range,
393 prefix,
394 suffix,
395 min_decimals,
396 max_decimals,
397 custom_formatter,
398 custom_parser,
399 update_while_editing,
400 } = self;
401
402 let shift = ui.input(|i| i.modifiers.shift_only());
403 // The widget has the same ID whether it's in edit or button mode.
404 let id = ui.next_auto_id();
405 let is_slow_speed = shift && ui.ctx().is_being_dragged(id);
406
407 // The following ensures that when a `DragValue` receives focus,
408 // it is immediately rendered in edit mode, rather than being rendered
409 // in button mode for just one frame. This is important for
410 // screen readers.
411 let is_kb_editing = ui.memory_mut(|mem| {
412 mem.interested_in_focus(id);
413 mem.has_focus(id)
414 });
415
416 if ui.memory_mut(|mem| mem.gained_focus(id)) {
417 ui.data_mut(|data| data.remove::<String>(id));
418 }
419
420 let old_value = get(&mut get_set_value);
421 let mut value = old_value;
422 let aim_rad = ui.input(|i| i.aim_radius() as f64);
423
424 let auto_decimals = (aim_rad / speed.abs()).log10().ceil().clamp(0.0, 15.0) as usize;
425 let auto_decimals = auto_decimals + is_slow_speed as usize;
426 let max_decimals = max_decimals
427 .unwrap_or(auto_decimals + 2)
428 .at_least(min_decimals);
429 let auto_decimals = auto_decimals.clamp(min_decimals, max_decimals);
430
431 let change = ui.input_mut(|input| {
432 let mut change = 0.0;
433
434 if is_kb_editing {
435 // This deliberately doesn't listen for left and right arrow keys,
436 // because when editing, these are used to move the caret.
437 // This behavior is consistent with other editable spinner/stepper
438 // implementations, such as Chromium's (for HTML5 number input).
439 // It is also normal for such controls to go directly into edit mode
440 // when they receive keyboard focus, and some screen readers
441 // assume this behavior, so having a separate mode for incrementing
442 // and decrementing, that supports all arrow keys, would be
443 // problematic.
444 change += input.count_and_consume_key(Modifiers::NONE, Key::ArrowUp) as f64
445 - input.count_and_consume_key(Modifiers::NONE, Key::ArrowDown) as f64;
446 }
447
448 #[cfg(feature = "accesskit")]
449 {
450 use accesskit::Action;
451 change += input.num_accesskit_action_requests(id, Action::Increment) as f64
452 - input.num_accesskit_action_requests(id, Action::Decrement) as f64;
453 }
454
455 change
456 });
457
458 #[cfg(feature = "accesskit")]
459 {
460 use accesskit::{Action, ActionData};
461 ui.input(|input| {
462 for request in input.accesskit_action_requests(id, Action::SetValue) {
463 if let Some(ActionData::NumericValue(new_value)) = request.data {
464 value = new_value;
465 }
466 }
467 });
468 }
469
470 if clamp_to_range {
471 value = clamp_value_to_range(value, range.clone());
472 }
473
474 if change != 0.0 {
475 value += speed * change;
476 value = emath::round_to_decimals(value, auto_decimals);
477 }
478
479 if old_value != value {
480 set(&mut get_set_value, value);
481 ui.data_mut(|data| data.remove::<String>(id));
482 }
483
484 let value_text = match custom_formatter {
485 Some(custom_formatter) => custom_formatter(value, auto_decimals..=max_decimals),
486 None => ui
487 .style()
488 .number_formatter
489 .format(value, auto_decimals..=max_decimals),
490 };
491
492 let text_style = ui.style().drag_value_text_style.clone();
493
494 if ui.memory(|mem| mem.lost_focus(id)) && !ui.input(|i| i.key_pressed(Key::Escape)) {
495 let value_text = ui.data_mut(|data| data.remove_temp::<String>(id));
496 if let Some(value_text) = value_text {
497 // We were editing the value as text last frame, but lost focus.
498 // Make sure we applied the last text value:
499 let parsed_value = parse(&custom_parser, &value_text);
500 if let Some(mut parsed_value) = parsed_value {
501 if clamp_to_range {
502 parsed_value = clamp_value_to_range(parsed_value, range.clone());
503 }
504 set(&mut get_set_value, parsed_value);
505 }
506 }
507 }
508
509 // some clones below are redundant if AccessKit is disabled
510 #[allow(clippy::redundant_clone)]
511 let mut response = if is_kb_editing {
512 let mut value_text = ui
513 .data_mut(|data| data.remove_temp::<String>(id))
514 .unwrap_or_else(|| value_text.clone());
515 let response = ui.add(
516 TextEdit::singleline(&mut value_text)
517 .clip_text(false)
518 .horizontal_align(ui.layout().horizontal_align())
519 .vertical_align(ui.layout().vertical_align())
520 .margin(ui.spacing().button_padding)
521 .min_size(ui.spacing().interact_size)
522 .id(id)
523 .desired_width(ui.spacing().interact_size.x)
524 .font(text_style),
525 );
526
527 let update = if update_while_editing {
528 // Update when the edit content has changed.
529 response.changed()
530 } else {
531 // Update only when the edit has lost focus.
532 response.lost_focus() && !ui.input(|i| i.key_pressed(Key::Escape))
533 };
534 if update {
535 let parsed_value = parse(&custom_parser, &value_text);
536 if let Some(mut parsed_value) = parsed_value {
537 if clamp_to_range {
538 parsed_value = clamp_value_to_range(parsed_value, range.clone());
539 }
540 set(&mut get_set_value, parsed_value);
541 }
542 }
543 ui.data_mut(|data| data.insert_temp(id, value_text));
544 response
545 } else {
546 let button = Button::new(
547 RichText::new(format!("{}{}{}", prefix, value_text.clone(), suffix))
548 .text_style(text_style),
549 )
550 .wrap_mode(TextWrapMode::Extend)
551 .sense(Sense::click_and_drag())
552 .min_size(ui.spacing().interact_size); // TODO(emilk): find some more generic solution to `min_size`
553
554 let cursor_icon = if value <= *range.start() {
555 CursorIcon::ResizeEast
556 } else if value < *range.end() {
557 CursorIcon::ResizeHorizontal
558 } else {
559 CursorIcon::ResizeWest
560 };
561
562 let response = ui.add(button);
563 let mut response = response.on_hover_cursor(cursor_icon);
564
565 if ui.style().explanation_tooltips {
566 response = response.on_hover_text(format!(
567 "{}{}{}\nDrag to edit or click to enter a value.\nPress 'Shift' while dragging for better control.",
568 prefix,
569 value as f32, // Show full precision value on-hover. TODO(emilk): figure out f64 vs f32
570 suffix
571 ));
572 }
573
574 if ui.input(|i| i.pointer.any_pressed() || i.pointer.any_released()) {
575 // Reset memory of preciely dagged value.
576 ui.data_mut(|data| data.remove::<f64>(id));
577 }
578
579 if response.clicked() {
580 ui.data_mut(|data| data.remove::<String>(id));
581 ui.memory_mut(|mem| mem.request_focus(id));
582 let mut state = TextEdit::load_state(ui.ctx(), id).unwrap_or_default();
583 state.cursor.set_char_range(Some(text::CCursorRange::two(
584 text::CCursor::default(),
585 text::CCursor::new(value_text.chars().count()),
586 )));
587 state.store(ui.ctx(), response.id);
588 } else if response.dragged() {
589 ui.ctx().set_cursor_icon(cursor_icon);
590
591 let mdelta = response.drag_delta();
592 let delta_points = mdelta.x - mdelta.y; // Increase to the right and up
593
594 let speed = if is_slow_speed { speed / 10.0 } else { speed };
595
596 let delta_value = delta_points as f64 * speed;
597
598 if delta_value != 0.0 {
599 // Since we round the value being dragged, we need to store the full precision value in memory:
600 let precise_value = ui.data_mut(|data| data.get_temp::<f64>(id));
601 let precise_value = precise_value.unwrap_or(value);
602 let precise_value = precise_value + delta_value;
603
604 let aim_delta = aim_rad * speed;
605 let rounded_new_value = emath::smart_aim::best_in_range_f64(
606 precise_value - aim_delta,
607 precise_value + aim_delta,
608 );
609 let rounded_new_value =
610 emath::round_to_decimals(rounded_new_value, auto_decimals);
611 // Dragging will always clamp the value to the range.
612 let rounded_new_value = clamp_value_to_range(rounded_new_value, range.clone());
613 set(&mut get_set_value, rounded_new_value);
614
615 ui.data_mut(|data| data.insert_temp::<f64>(id, precise_value));
616 }
617 }
618
619 response
620 };
621
622 response.changed = get(&mut get_set_value) != old_value;
623
624 response.widget_info(|| WidgetInfo::drag_value(ui.is_enabled(), value));
625
626 #[cfg(feature = "accesskit")]
627 ui.ctx().accesskit_node_builder(response.id, |builder| {
628 use accesskit::Action;
629 // If either end of the range is unbounded, it's better
630 // to leave the corresponding AccessKit field set to None,
631 // to allow for platform-specific default behavior.
632 if range.start().is_finite() {
633 builder.set_min_numeric_value(*range.start());
634 }
635 if range.end().is_finite() {
636 builder.set_max_numeric_value(*range.end());
637 }
638 builder.set_numeric_value_step(speed);
639 builder.add_action(Action::SetValue);
640 if value < *range.end() {
641 builder.add_action(Action::Increment);
642 }
643 if value > *range.start() {
644 builder.add_action(Action::Decrement);
645 }
646 // The name field is set to the current value by the button,
647 // but we don't want it set that way on this widget type.
648 builder.clear_name();
649 // Always expose the value as a string. This makes the widget
650 // more stable to accessibility users as it switches
651 // between edit and button modes. This is particularly important
652 // for VoiceOver on macOS; if the value is not exposed as a string
653 // when the widget is in button mode, then VoiceOver speaks
654 // the value (or a percentage if the widget has a clamp range)
655 // when the widget loses focus, overriding the announcement
656 // of the newly focused widget. This is certainly a VoiceOver bug,
657 // but it's good to make our software work as well as possible
658 // with existing assistive technology. However, if the widget
659 // has a prefix and/or suffix, expose those when in button mode,
660 // just as they're exposed on the screen. This triggers the
661 // VoiceOver bug just described, but exposing all information
662 // is more important, and at least we can avoid the bug
663 // for instances of the widget with no prefix or suffix.
664 //
665 // The value is exposed as a string by the text edit widget
666 // when in edit mode.
667 if !is_kb_editing {
668 let value_text = format!("{prefix}{value_text}{suffix}");
669 builder.set_value(value_text);
670 }
671 });
672
673 response
674 }
675}
676
677fn parse(custom_parser: &Option<NumParser<'_>>, value_text: &str) -> Option<f64> {
678 match &custom_parser {
679 Some(parser) => parser(value_text),
680 None => default_parser(value_text),
681 }
682}
683
684/// The default egui parser of numbers.
685///
686/// It ignored whitespaces anywhere in the input, and treats the special minus character (U+2212) as a normal minus.
687fn default_parser(text: &str) -> Option<f64> {
688 let text: String = text
689 .chars()
690 // Ignore whitespace (trailing, leading, and thousands separators):
691 .filter(|c| !c.is_whitespace())
692 // Replace special minus character with normal minus (hyphen):
693 .map(|c| if c == '−' { '-' } else { c })
694 .collect();
695
696 text.parse().ok()
697}
698
699fn clamp_value_to_range(x: f64, range: RangeInclusive<f64>) -> f64 {
700 let (mut min, mut max) = (*range.start(), *range.end());
701
702 if min.total_cmp(&max) == Ordering::Greater {
703 (min, max) = (max, min);
704 }
705
706 match x.total_cmp(&min) {
707 Ordering::Less | Ordering::Equal => min,
708 Ordering::Greater => match x.total_cmp(&max) {
709 Ordering::Greater | Ordering::Equal => max,
710 Ordering::Less => x,
711 },
712 }
713}
714
715#[cfg(test)]
716mod tests {
717 use super::clamp_value_to_range;
718
719 macro_rules! total_assert_eq {
720 ($a:expr, $b:expr) => {
721 assert!(
722 matches!($a.total_cmp(&$b), std::cmp::Ordering::Equal),
723 "{} != {}",
724 $a,
725 $b
726 );
727 };
728 }
729
730 #[test]
731 fn test_total_cmp_clamp_value_to_range() {
732 total_assert_eq!(0.0_f64, clamp_value_to_range(-0.0, 0.0..=f64::MAX));
733 total_assert_eq!(-0.0_f64, clamp_value_to_range(0.0, -1.0..=-0.0));
734 total_assert_eq!(-1.0_f64, clamp_value_to_range(-25.0, -1.0..=1.0));
735 total_assert_eq!(5.0_f64, clamp_value_to_range(5.0, -1.0..=10.0));
736 total_assert_eq!(15.0_f64, clamp_value_to_range(25.0, -1.0..=15.0));
737 total_assert_eq!(1.0_f64, clamp_value_to_range(1.0, 1.0..=10.0));
738 total_assert_eq!(10.0_f64, clamp_value_to_range(10.0, 1.0..=10.0));
739 total_assert_eq!(5.0_f64, clamp_value_to_range(5.0, 10.0..=1.0));
740 total_assert_eq!(5.0_f64, clamp_value_to_range(15.0, 5.0..=1.0));
741 total_assert_eq!(1.0_f64, clamp_value_to_range(-5.0, 5.0..=1.0));
742 }
743
744 #[test]
745 fn test_default_parser() {
746 assert_eq!(super::default_parser("123"), Some(123.0));
747
748 assert_eq!(super::default_parser("1.23"), Some(1.230));
749
750 assert_eq!(
751 super::default_parser(" 1.23 "),
752 Some(1.230),
753 "We should handle leading and trailing spaces"
754 );
755
756 assert_eq!(
757 super::default_parser("1 234 567"),
758 Some(1_234_567.0),
759 "We should handle thousands separators using half-space"
760 );
761
762 assert_eq!(
763 super::default_parser("-1.23"),
764 Some(-1.23),
765 "Should handle normal hyphen as minus character"
766 );
767 assert_eq!(
768 super::default_parser("−1.23"),
769 Some(-1.23),
770 "Should handle special minus character (https://www.compart.com/en/unicode/U+2212)"
771 );
772 }
773}