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