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