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}