Skip to main content

egui/widgets/
drag_value.rs

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