diff --git a/CHANGELOG.md b/CHANGELOG.md index 702ef444..9cb01f62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG * Added `custom_formatter` method for `Slider` and `DragValue` ([#1851](https://github.com/emilk/egui/issues/1851)). * Added `RawInput::has_focus` which backends can set to indicate whether the UI as a whole has the keyboard focus ([#1859](https://github.com/emilk/egui/pull/1859)). * Added `PointerState::button_double_clicked()` and `PointerState::button_triple_clicked()` ([#1906](https://github.com/emilk/egui/issues/1906)). +* Added `custom_formatter`, `binary`, `octal`, and `hexadecimal` to `DragValue` and `Slider` ([#1953](https://github.com/emilk/egui/issues/1953)) ### Changed * MSRV (Minimum Supported Rust Version) is now `1.61.0` ([#1846](https://github.com/emilk/egui/pull/1846)). diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index 2951b25f..7a6af298 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -28,6 +28,7 @@ impl MonoState { // ---------------------------------------------------------------------------- type NumFormatter<'a> = Box) -> String>; +type NumParser<'a> = Box Option>; // ---------------------------------------------------------------------------- @@ -61,6 +62,7 @@ pub struct DragValue<'a> { min_decimals: usize, max_decimals: Option, custom_formatter: Option>, + custom_parser: Option>, } impl<'a> DragValue<'a> { @@ -91,6 +93,7 @@ impl<'a> DragValue<'a> { min_decimals: 0, max_decimals: None, custom_formatter: None, + custom_parser: None, } } @@ -157,10 +160,35 @@ impl<'a> DragValue<'a> { /// A custom formatter takes a `f64` for the numeric value and a `RangeInclusive` representing /// the decimal range i.e. minimum and maximum number of decimal places shown. /// + /// See also: [`DragValue::custom_parser`] + /// /// ``` /// # egui::__run_test_ui(|ui| { - /// # let mut my_i64: i64 = 0; - /// ui.add(egui::DragValue::new(&mut my_i64).custom_formatter(|n, _| format!("{:X}", n as i64))); + /// # let mut my_i32: i32 = 0; + /// ui.add(egui::DragValue::new(&mut my_i32) + /// .clamp_range(0..=((60 * 60 * 24) - 1)) + /// .custom_formatter(|n, _| { + /// let n = n as i32; + /// let hours = n / (60 * 60); + /// let mins = (n / 60) % 60; + /// let secs = n % 60; + /// format!("{hours:02}:{mins:02}:{secs:02}") + /// }) + /// .custom_parser(|s| { + /// let parts: Vec<&str> = s.split(':').collect(); + /// if parts.len() == 3 { + /// parts[0].parse::().and_then(|h| { + /// parts[1].parse::().and_then(|m| { + /// parts[2].parse::().map(|s| { + /// ((h * 60 * 60) + (m * 60) + s) as f64 + /// }) + /// }) + /// }) + /// .ok() + /// } else { + /// None + /// } + /// })); /// # }); /// ``` pub fn custom_formatter( @@ -170,6 +198,160 @@ impl<'a> DragValue<'a> { self.custom_formatter = Some(Box::new(formatter)); self } + + /// Set custom parser defining how the text input is parsed into a number. + /// + /// A custom parser takes an `&str` to parse into a number and returns a `f64` if it was successfully parsed + /// or `None` otherwise. + /// + /// See also: [`DragValue::custom_formatter`] + /// + /// ``` + /// # egui::__run_test_ui(|ui| { + /// # let mut my_i32: i32 = 0; + /// ui.add(egui::DragValue::new(&mut my_i32) + /// .clamp_range(0..=((60 * 60 * 24) - 1)) + /// .custom_formatter(|n, _| { + /// let n = n as i32; + /// let hours = n / (60 * 60); + /// let mins = (n / 60) % 60; + /// let secs = n % 60; + /// format!("{hours:02}:{mins:02}:{secs:02}") + /// }) + /// .custom_parser(|s| { + /// let parts: Vec<&str> = s.split(':').collect(); + /// if parts.len() == 3 { + /// parts[0].parse::().and_then(|h| { + /// parts[1].parse::().and_then(|m| { + /// parts[2].parse::().map(|s| { + /// ((h * 60 * 60) + (m * 60) + s) as f64 + /// }) + /// }) + /// }) + /// .ok() + /// } else { + /// None + /// } + /// })); + /// # }); + /// ``` + pub fn custom_parser(mut self, parser: impl 'a + Fn(&str) -> Option) -> Self { + self.custom_parser = Some(Box::new(parser)); + self + } + + /// Set `custom_formatter` and `custom_parser` to display and parse numbers as binary integers. Floating point + /// numbers are *not* supported. + /// + /// `min_width` specifies the minimum number of displayed digits; if the number is shorter than this, it will be + /// prefixed with additional 0s to match `min_width`. + /// + /// If `twos_complement` is true, negative values will be displayed as the 2's complement representation. Otherwise + /// they will be prefixed with a '-' sign. + /// + /// # Panics + /// + /// Panics if `min_width` is 0. + /// + /// ``` + /// # egui::__run_test_ui(|ui| { + /// # let mut my_i32: i32 = 0; + /// ui.add(egui::DragValue::new(&mut my_i32).binary(64, false)); + /// # }); + /// ``` + pub fn binary(self, min_width: usize, twos_complement: bool) -> Self { + assert!( + min_width > 0, + "DragValue::binary: `min_width` must be greater than 0" + ); + if twos_complement { + self.custom_formatter(move |n, _| format!("{:0>min_width$b}", n as i64)) + } else { + self.custom_formatter(move |n, _| { + let sign = if n < 0.0 { "-" } else { "" }; + format!("{sign}{:0>min_width$b}", n.abs() as i64) + }) + } + .custom_parser(|s| i64::from_str_radix(s, 2).map(|n| n as f64).ok()) + } + + /// Set `custom_formatter` and `custom_parser` to display and parse numbers as octal integers. Floating point + /// numbers are *not* supported. + /// + /// `min_width` specifies the minimum number of displayed digits; if the number is shorter than this, it will be + /// prefixed with additional 0s to match `min_width`. + /// + /// If `twos_complement` is true, negative values will be displayed as the 2's complement representation. Otherwise + /// they will be prefixed with a '-' sign. + /// + /// # Panics + /// + /// Panics if `min_width` is 0. + /// + /// ``` + /// # egui::__run_test_ui(|ui| { + /// # let mut my_i32: i32 = 0; + /// ui.add(egui::DragValue::new(&mut my_i32).octal(22, false)); + /// # }); + /// ``` + pub fn octal(self, min_width: usize, twos_complement: bool) -> Self { + assert!( + min_width > 0, + "DragValue::octal: `min_width` must be greater than 0" + ); + if twos_complement { + self.custom_formatter(move |n, _| format!("{:0>min_width$o}", n as i64)) + } else { + self.custom_formatter(move |n, _| { + let sign = if n < 0.0 { "-" } else { "" }; + format!("{sign}{:0>min_width$o}", n.abs() as i64) + }) + } + .custom_parser(|s| i64::from_str_radix(s, 8).map(|n| n as f64).ok()) + } + + /// Set `custom_formatter` and `custom_parser` to display and parse numbers as hexadecimal integers. Floating point + /// numbers are *not* supported. + /// + /// `min_width` specifies the minimum number of displayed digits; if the number is shorter than this, it will be + /// prefixed with additional 0s to match `min_width`. + /// + /// If `twos_complement` is true, negative values will be displayed as the 2's complement representation. Otherwise + /// they will be prefixed with a '-' sign. + /// + /// # Panics + /// + /// Panics if `min_width` is 0. + /// + /// ``` + /// # egui::__run_test_ui(|ui| { + /// # let mut my_i32: i32 = 0; + /// ui.add(egui::DragValue::new(&mut my_i32).hexadecimal(16, false, true)); + /// # }); + /// ``` + pub fn hexadecimal(self, min_width: usize, twos_complement: bool, upper: bool) -> Self { + assert!( + min_width > 0, + "DragValue::hexadecimal: `min_width` must be greater than 0" + ); + match (twos_complement, upper) { + (true, true) => { + self.custom_formatter(move |n, _| format!("{:0>min_width$X}", n as i64)) + } + (true, false) => { + self.custom_formatter(move |n, _| format!("{:0>min_width$x}", n as i64)) + } + (false, true) => self.custom_formatter(move |n, _| { + let sign = if n < 0.0 { "-" } else { "" }; + format!("{sign}{:0>min_width$X}", n.abs() as i64) + }), + (false, false) => self.custom_formatter(move |n, _| { + let sign = if n < 0.0 { "-" } else { "" }; + format!("{sign}{:0>min_width$x}", n.abs() as i64) + }), + } + .custom_parser(|s| i64::from_str_radix(s, 16).map(|n| n as f64).ok()) + } } impl<'a> Widget for DragValue<'a> { @@ -183,6 +365,7 @@ impl<'a> Widget for DragValue<'a> { min_decimals, max_decimals, custom_formatter, + custom_parser, } = self; let shift = ui.input().modifiers.shift_only(); @@ -228,7 +411,11 @@ impl<'a> Widget for DragValue<'a> { .desired_width(button_width) .font(TextStyle::Monospace), ); - if let Ok(parsed_value) = value_text.parse() { + let parsed_value = match custom_parser { + Some(parser) => parser(&value_text), + None => value_text.parse().ok(), + }; + if let Some(parsed_value) = parsed_value { let parsed_value = clamp_to_range(parsed_value, clamp_range); set(&mut get_set_value, parsed_value); } diff --git a/crates/egui/src/widgets/slider.rs b/crates/egui/src/widgets/slider.rs index 1534a5bd..d49d3dac 100644 --- a/crates/egui/src/widgets/slider.rs +++ b/crates/egui/src/widgets/slider.rs @@ -7,6 +7,7 @@ use crate::*; // ---------------------------------------------------------------------------- type NumFormatter<'a> = Box) -> String>; +type NumParser<'a> = Box Option>; // ---------------------------------------------------------------------------- @@ -82,6 +83,7 @@ pub struct Slider<'a> { min_decimals: usize, max_decimals: Option, custom_formatter: Option>, + custom_parser: Option>, } impl<'a> Slider<'a> { @@ -126,6 +128,7 @@ impl<'a> Slider<'a> { min_decimals: 0, max_decimals: None, custom_formatter: None, + custom_parser: None, } } @@ -254,10 +257,34 @@ impl<'a> Slider<'a> { /// A custom formatter takes a `f64` for the numeric value and a `RangeInclusive` representing /// the decimal range i.e. minimum and maximum number of decimal places shown. /// + /// See also: [`DragValue::custom_parser`] + /// /// ``` /// # egui::__run_test_ui(|ui| { - /// # let mut my_i64: i64 = 0; - /// ui.add(egui::Slider::new(&mut my_i64, 0..=100).custom_formatter(|n, _| format!("{:X}", n as i64))); + /// # let mut my_i32: i32 = 0; + /// ui.add(egui::Slider::new(&mut my_i32, 0..=((60 * 60 * 24) - 1)) + /// .custom_formatter(|n, _| { + /// let n = n as i32; + /// let hours = n / (60 * 60); + /// let mins = (n / 60) % 60; + /// let secs = n % 60; + /// format!("{hours:02}:{mins:02}:{secs:02}") + /// }) + /// .custom_parser(|s| { + /// let parts: Vec<&str> = s.split(':').collect(); + /// if parts.len() == 3 { + /// parts[0].parse::().and_then(|h| { + /// parts[1].parse::().and_then(|m| { + /// parts[2].parse::().map(|s| { + /// ((h * 60 * 60) + (m * 60) + s) as f64 + /// }) + /// }) + /// }) + /// .ok() + /// } else { + /// None + /// } + /// })); /// # }); /// ``` pub fn custom_formatter( @@ -268,6 +295,159 @@ impl<'a> Slider<'a> { self } + /// Set custom parser defining how the text input is parsed into a number. + /// + /// 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`] + /// + /// ``` + /// # egui::__run_test_ui(|ui| { + /// # let mut my_i32: i32 = 0; + /// ui.add(egui::Slider::new(&mut my_i32, 0..=((60 * 60 * 24) - 1)) + /// .custom_formatter(|n, _| { + /// let n = n as i32; + /// let hours = n / (60 * 60); + /// let mins = (n / 60) % 60; + /// let secs = n % 60; + /// format!("{hours:02}:{mins:02}:{secs:02}") + /// }) + /// .custom_parser(|s| { + /// let parts: Vec<&str> = s.split(':').collect(); + /// if parts.len() == 3 { + /// parts[0].parse::().and_then(|h| { + /// parts[1].parse::().and_then(|m| { + /// parts[2].parse::().map(|s| { + /// ((h * 60 * 60) + (m * 60) + s) as f64 + /// }) + /// }) + /// }) + /// .ok() + /// } else { + /// None + /// } + /// })); + /// # }); + /// ``` + pub fn custom_parser(mut self, parser: impl 'a + Fn(&str) -> Option) -> Self { + self.custom_parser = Some(Box::new(parser)); + self + } + + /// Set `custom_formatter` and `custom_parser` to display and parse numbers as binary integers. Floating point + /// numbers are *not* supported. + /// + /// `min_width` specifies the minimum number of displayed digits; if the number is shorter than this, it will be + /// prefixed with additional 0s to match `min_width`. + /// + /// If `twos_complement` is true, negative values will be displayed as the 2's complement representation. Otherwise + /// they will be prefixed with a '-' sign. + /// + /// # Panics + /// + /// Panics if `min_width` is 0. + /// + /// ``` + /// # egui::__run_test_ui(|ui| { + /// # let mut my_i32: i32 = 0; + /// ui.add(egui::Slider::new(&mut my_i32, -100..=100).binary(64, false)); + /// # }); + /// ``` + pub fn binary(self, min_width: usize, twos_complement: bool) -> Self { + assert!( + min_width > 0, + "Slider::binary: `min_width` must be greater than 0" + ); + if twos_complement { + self.custom_formatter(move |n, _| format!("{:0>min_width$b}", n as i64)) + } else { + self.custom_formatter(move |n, _| { + let sign = if n < 0.0 { "-" } else { "" }; + format!("{sign}{:0>min_width$b}", n.abs() as i64) + }) + } + .custom_parser(|s| i64::from_str_radix(s, 2).map(|n| n as f64).ok()) + } + + /// Set `custom_formatter` and `custom_parser` to display and parse numbers as octal integers. Floating point + /// numbers are *not* supported. + /// + /// `min_width` specifies the minimum number of displayed digits; if the number is shorter than this, it will be + /// prefixed with additional 0s to match `min_width`. + /// + /// If `twos_complement` is true, negative values will be displayed as the 2's complement representation. Otherwise + /// they will be prefixed with a '-' sign. + /// + /// # Panics + /// + /// Panics if `min_width` is 0. + /// + /// ``` + /// # egui::__run_test_ui(|ui| { + /// # let mut my_i32: i32 = 0; + /// ui.add(egui::Slider::new(&mut my_i32, -100..=100).octal(22, false)); + /// # }); + /// ``` + pub fn octal(self, min_width: usize, twos_complement: bool) -> Self { + assert!( + min_width > 0, + "Slider::octal: `min_width` must be greater than 0" + ); + if twos_complement { + self.custom_formatter(move |n, _| format!("{:0>min_width$o}", n as i64)) + } else { + self.custom_formatter(move |n, _| { + let sign = if n < 0.0 { "-" } else { "" }; + format!("{sign}{:0>min_width$o}", n.abs() as i64) + }) + } + .custom_parser(|s| i64::from_str_radix(s, 8).map(|n| n as f64).ok()) + } + + /// Set `custom_formatter` and `custom_parser` to display and parse numbers as hexadecimal integers. Floating point + /// numbers are *not* supported. + /// + /// `min_width` specifies the minimum number of displayed digits; if the number is shorter than this, it will be + /// prefixed with additional 0s to match `min_width`. + /// + /// If `twos_complement` is true, negative values will be displayed as the 2's complement representation. Otherwise + /// they will be prefixed with a '-' sign. + /// + /// # Panics + /// + /// Panics if `min_width` is 0. + /// + /// ``` + /// # egui::__run_test_ui(|ui| { + /// # let mut my_i32: i32 = 0; + /// ui.add(egui::Slider::new(&mut my_i32, -100..=100).hexadecimal(16, false, true)); + /// # }); + /// ``` + pub fn hexadecimal(self, min_width: usize, twos_complement: bool, upper: bool) -> Self { + assert!( + min_width > 0, + "Slider::hexadecimal: `min_width` must be greater than 0" + ); + match (twos_complement, upper) { + (true, true) => { + self.custom_formatter(move |n, _| format!("{:0>min_width$X}", n as i64)) + } + (true, false) => { + self.custom_formatter(move |n, _| format!("{:0>min_width$x}", n as i64)) + } + (false, true) => self.custom_formatter(move |n, _| { + let sign = if n < 0.0 { "-" } else { "" }; + format!("{sign}{:0>min_width$X}", n.abs() as i64) + }), + (false, false) => self.custom_formatter(move |n, _| { + let sign = if n < 0.0 { "-" } else { "" }; + format!("{sign}{:0>min_width$x}", n.abs() as i64) + }), + } + .custom_parser(|s| i64::from_str_radix(s, 16).map(|n| n as f64).ok()) + } + /// Helper: equivalent to `self.precision(0).smallest_positive(1.0)`. /// If you use one of the integer constructors (e.g. `Slider::i32`) this is called for you, /// but if you want to have a slider for picking integer values in an `Slider::f64`, use this. @@ -493,17 +673,20 @@ impl<'a> Slider<'a> { }; let mut value = self.get_value(); let response = ui.add({ - let dv = DragValue::new(&mut value) + let mut dv = DragValue::new(&mut value) .speed(speed) .clamp_range(self.clamp_range()) .min_decimals(self.min_decimals) .max_decimals_opt(self.max_decimals) .suffix(self.suffix.clone()) .prefix(self.prefix.clone()); - match &self.custom_formatter { - Some(fmt) => dv.custom_formatter(fmt), - None => dv, + if let Some(fmt) = &self.custom_formatter { + dv = dv.custom_formatter(fmt); + }; + if let Some(parser) = &self.custom_parser { + dv = dv.custom_parser(parser); } + dv }); if value != self.get_value() { self.set_value(value);