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