Skip to main content

egui/widgets/
drag_value.rs

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