Add `Style::number_formatter` as the default used by `DragValue` (#4740)

This allows users to customize how numbers are converted into strings in
a `DragValue`, as a global default.

This can be used (for instance) to show thousands separators, or use `,`
as a decimal separator.
This commit is contained in:
Emil Ernerfeldt 2024-06-30 17:17:15 +02:00 committed by GitHub
parent e297a1d107
commit dc1f032846
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 76 additions and 7 deletions

View File

@ -2,7 +2,7 @@
#![allow(clippy::if_same_then_else)]
use std::collections::BTreeMap;
use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc};
use epaint::{Rounding, Shadow, Stroke};
@ -11,6 +11,51 @@ use crate::{
RichText, WidgetText,
};
/// How to format numbers in e.g. a [`crate::DragValue`].
#[derive(Clone)]
pub struct NumberFormatter(
Arc<dyn 'static + Sync + Send + Fn(f64, RangeInclusive<usize>) -> String>,
);
impl NumberFormatter {
/// The first argument is the number to be formatted.
/// The second argument is the range of the number of decimals to show.
///
/// See [`Self::format`] for the meaning of the `decimals` argument.
#[inline]
pub fn new(
formatter: impl 'static + Sync + Send + Fn(f64, RangeInclusive<usize>) -> String,
) -> Self {
Self(Arc::new(formatter))
}
/// Format the given number with the given number of decimals.
///
/// Decimals are counted after the decimal point.
///
/// The minimum number of decimals is usually automatically calculated
/// from the sensitivity of the [`crate::DragValue`] and will usually be respected (e.g. include trailing zeroes),
/// but if the given value requires more decimals to represent accurately,
/// more decimals will be shown, up to the given max.
#[inline]
pub fn format(&self, value: f64, decimals: RangeInclusive<usize>) -> String {
(self.0)(value, decimals)
}
}
impl std::fmt::Debug for NumberFormatter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("NumberFormatter")
}
}
impl PartialEq for NumberFormatter {
#[inline]
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}
// ----------------------------------------------------------------------------
/// Alias for a [`FontId`] (font of a certain size).
@ -182,6 +227,12 @@ pub struct Style {
/// The style to use for [`DragValue`] text.
pub drag_value_text_style: TextStyle,
/// How to format numbers as strings, e.g. in a [`crate::DragValue`].
///
/// You can override this to e.g. add thousands separators.
#[cfg_attr(feature = "serde", serde(skip))]
pub number_formatter: NumberFormatter,
/// If set, labels, buttons, etc. will use this to determine whether to wrap the text at the
/// right edge of the [`Ui`] they are in. By default, this is `None`.
///
@ -231,6 +282,12 @@ pub struct Style {
pub always_scroll_the_only_direction: bool,
}
#[test]
fn style_impl_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Style>();
}
impl Style {
// TODO(emilk): rename style.interact() to maybe… `style.interactive` ?
/// Use this style for interactive things.
@ -1060,6 +1117,7 @@ impl Default for Style {
override_text_style: None,
text_styles: default_text_styles(),
drag_value_text_style: TextStyle::Button,
number_formatter: NumberFormatter(Arc::new(emath::format_with_decimals_in_range)),
wrap: None,
wrap_mode: None,
spacing: Spacing::default(),
@ -1355,6 +1413,7 @@ impl Style {
override_text_style,
text_styles,
drag_value_text_style,
number_formatter: _, // can't change callbacks in the UI
wrap: _,
wrap_mode: _,
spacing,

View File

@ -176,6 +176,8 @@ impl<'a> DragValue<'a> {
/// A custom formatter takes a `f64` for the numeric value and a `RangeInclusive<usize>` representing
/// the decimal range i.e. minimum and maximum number of decimal places shown.
///
/// The default formatter is [`Style::number_formatter`].
///
/// See also: [`DragValue::custom_parser`]
///
/// ```
@ -481,7 +483,10 @@ impl<'a> Widget for DragValue<'a> {
let value_text = match custom_formatter {
Some(custom_formatter) => custom_formatter(value, auto_decimals..=max_decimals),
None => emath::format_with_decimals_in_range(value, auto_decimals..=max_decimals),
None => ui
.style()
.number_formatter
.format(value, auto_decimals..=max_decimals),
};
let text_style = ui.style().drag_value_text_style.clone();
@ -676,8 +681,11 @@ fn parse(custom_parser: &Option<NumParser<'_>>, value_text: &str) -> Option<f64>
}
}
fn default_parser(value_text: &str) -> Option<f64> {
let value_text: String = value_text
/// The default egui parser of numbers.
///
/// It ignored whitespaces anywhere in the input, and treats the special minus character (U+2212) as a normal minus.
fn default_parser(text: &str) -> Option<f64> {
let text: String = text
.chars()
// Ignore whitespace (trailing, leading, and thousands separators):
.filter(|c| !c.is_whitespace())
@ -685,7 +693,7 @@ fn default_parser(value_text: &str) -> Option<f64> {
.map(|c| if c == '' { '-' } else { c })
.collect();
value_text.parse().ok()
text.parse().ok()
}
fn clamp_value_to_range(x: f64, range: RangeInclusive<f64>) -> f64 {

View File

@ -318,7 +318,9 @@ impl<'a> Slider<'a> {
/// A custom formatter takes a `f64` for the numeric value and a `RangeInclusive<usize>` representing
/// the decimal range i.e. minimum and maximum number of decimal places shown.
///
/// See also: [`DragValue::custom_parser`]
/// The default formatter is [`Style::number_formatter`].
///
/// See also: [`Slider::custom_parser`]
///
/// ```
/// # egui::__run_test_ui(|ui| {
@ -361,7 +363,7 @@ impl<'a> Slider<'a> {
/// A custom parser takes an `&str` to parse into a number and returns `Some` if it was successfully parsed
/// or `None` otherwise.
///
/// See also: [`DragValue::custom_formatter`]
/// See also: [`Slider::custom_formatter`]
///
/// ```
/// # egui::__run_test_ui(|ui| {