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