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}