Skip to content

Commit

Permalink
Add Style::number_formatter as the default used by DragValue (#4740)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
emilk authored Jun 30, 2024
1 parent e297a1d commit dc1f032
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 7 deletions.
61 changes: 60 additions & 1 deletion crates/egui/src/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -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).
Expand Down Expand Up @@ -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`.
///
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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,
Expand Down
16 changes: 12 additions & 4 deletions crates/egui/src/widgets/drag_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`]
///
/// ```
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -676,16 +681,19 @@ 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())
// Replace special minus character with normal minus (hyphen):
.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 {
Expand Down
6 changes: 4 additions & 2 deletions crates/egui/src/widgets/slider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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| {
Expand Down Expand Up @@ -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| {
Expand Down

0 comments on commit dc1f032

Please sign in to comment.