-
Notifications
You must be signed in to change notification settings - Fork 117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Typing a number into the number_input
doesn't work properly when using a custom number type to handle formatting precision
#319
Comments
yeah this is due to how Floats work in Standard and display in Standard Rust. It was one of our concerns but there is not much we can do about it without enforcing Custom types or spiting and enforcing Float formatting options. Floating points generally are not accurate. there are some libraries that do make their floats split into integers and display and output them as floats since this is more Reliable than an actual float. |
Yes, I do understand why it doesn't work well with floating point numbers, which is why I have been trying to use a custom type which stores the underlying value as an integer. It's a method that worked in the GUI framework that I am porting my application from, at least. My real issue is the latter part of my original post - when I try and use a custom type, the keyboard input stops working properly. |
yeah it fails to work due to it not detecting it as a float so it wont allow the 0.1 for custom types that are integers acting as a float. I think the best way to fix it would be to Separate it into its own number input. |
Hello, I think it comes from several mistakes in your code: Your definition of one is strange, shouldn't it be 10 rather than 1 ? (Should not break the widget but remark seems pertinent) I hope this solves your issue |
That gets a lot closer to the desired functionality - both your points about my implementation were valid. A few outstanding issues:
Full code used in the above examplesuse iced::{
alignment::Vertical,
widget::{center, row},
Element,
};
use iced_aw::{iced_fonts, number_input};
use num_traits::Bounded;
#[derive(Default, Debug)]
pub struct NumberInputDecimalTest {
delay: Duration,
}
#[derive(Debug, Clone)]
pub enum Message {
NumInpChanged(Duration),
NumInpSubmit,
}
impl NumberInputDecimalTest {
fn update(&mut self, message: self::Message) {
match message {
Message::NumInpChanged(val) => {
println!("Value changed to {:?}", val);
self.delay = val;
}
Message::NumInpSubmit => println!("Input submitted; value: {:?}", self.delay),
}
}
fn view(&self) -> Element<Message> {
let delay_input = number_input(
self.delay,
Duration::min_value()..=Duration::max_value(),
Message::NumInpChanged,
)
.on_submit(Message::NumInpSubmit)
.style(number_input::number_input::primary)
.step(Duration::new(1).unwrap());
center(
row!["Delay (s):", delay_input]
.spacing(15)
.align_y(Vertical::Center),
)
.into()
}
}
// This struct is a custom number type which attempts to represent the following logic:
// - The minimum `Duration` is 0 seconds
// - The maximum `Duration` is 10 seconds
// - The step size of a duration is in 10ths of a second
//
// The underlying value is stored as an integer number of tenths of a second so that the value
// can be correctly and exactly be formatted for display, compared etc.
#[derive(Debug, Clone, Copy, PartialEq, Default, PartialOrd)]
pub struct Duration(u32);
impl Duration {
pub fn new(gain: u32) -> Option<Self> {
if gain >= *Self::min_value() && gain <= *Self::max_value() {
Some(Self(gain))
} else {
None
}
}
}
impl std::fmt::Display for Duration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:.1}", self.0 as f32 / 10.0)
}
}
impl std::ops::Deref for Duration {
type Target = u32;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl num_traits::Num for Duration {
type FromStrRadixErr = <f32 as num_traits::Num>::FromStrRadixErr;
fn from_str_radix(str: &str, radix: u32) -> Result<Self, Self::FromStrRadixErr> {
<f32 as num_traits::Num>::from_str_radix(str, radix).map(|v| {
let inner = v * 10.0;
Self(inner as u32)
})
}
}
impl num_traits::Bounded for Duration {
fn min_value() -> Self {
Self(0)
}
fn max_value() -> Self {
Self(100)
}
}
impl std::str::FromStr for Duration {
type Err = <f32 as std::str::FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
<f32 as std::str::FromStr>::from_str(s).map(|v| {
let inner = v * 10.0;
Self(inner as u32)
})
}
}
impl num_traits::Zero for Duration {
fn zero() -> Self {
Self(0)
}
fn is_zero(&self) -> bool {
self.0 == 0
}
}
impl num_traits::One for Duration {
fn one() -> Self {
Self(10)
}
fn is_one(&self) -> bool
where
Self: PartialEq,
{
self.0 == 10
}
}
impl std::ops::Add for Duration {
type Output = Duration;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl std::ops::Sub for Duration {
type Output = Duration;
fn sub(self, rhs: Self) -> Self::Output {
Self(self.0 - rhs.0)
}
}
impl std::ops::Mul for Duration {
type Output = Duration;
fn mul(self, rhs: Self) -> Self::Output {
Self(self.0 * rhs.0)
}
}
impl std::ops::Div for Duration {
type Output = Duration;
fn div(self, rhs: Self) -> Self::Output {
Self(self.0 / rhs.0)
}
}
impl std::ops::Rem for Duration {
type Output = Duration;
fn rem(self, rhs: Self) -> Self::Output {
Self(self.0 % rhs.0)
}
}
impl std::ops::AddAssign for Duration {
fn add_assign(&mut self, rhs: Self) {
self.0 = self.0 + rhs.0
}
}
impl std::ops::SubAssign for Duration {
fn sub_assign(&mut self, rhs: Self) {
self.0 = self.0 - rhs.0
}
}
impl std::ops::MulAssign for Duration {
fn mul_assign(&mut self, rhs: Self) {
self.0 = self.0 * rhs.0
}
}
impl std::ops::DivAssign for Duration {
fn div_assign(&mut self, rhs: Self) {
self.0 = self.0 / rhs.0
}
}
impl std::ops::RemAssign for Duration {
fn rem_assign(&mut self, rhs: Self) {
self.0 = self.0 % rhs.0
}
}
fn main() -> iced::Result {
iced::application(
"Number Input Decimal Test",
NumberInputDecimalTest::update,
NumberInputDecimalTest::view,
)
.window_size((400.0, 100.0))
.font(iced_fonts::REQUIRED_FONT_BYTES)
.run()
} |
Thanks, I overlooked that, now this bit works well. Quick bit of new code to replace the original FromStr impl#[derive(Debug)]
pub enum ParseDurationError {
Invalid,
}
impl std::fmt::Display for ParseDurationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Invalid => write!(f, "Invalid duration"),
}
}
}
impl std::error::Error for ParseDurationError {}
impl std::str::FromStr for Duration {
type Err = ParseDurationError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let inner = <f32 as std::str::FromStr>::from_str(s)
.map(|v| v * 10.0)
.map_err(|_| ParseDurationError::Invalid)?;
match inner.fract() {
0.0 => Ok(Self(inner as u32)),
_ => Err(ParseDurationError::Invalid),
}
}
}
In short, it's not letting me type in valid values.
In terms of prior art, I haven't got my equivalent Qt app to hand, but I am pretty sure that in Qt the input automatically reverts to zero (actually fills in the input with zero, rather than leaving it blank). I wonder if it would be even better to revert to T::default (thinking ahead, I know that I've got another number_input I need to write which has a NonZeroSomethingOrOther type!)
Absolutely, thanks for your help so far :) |
For the point 2 can you explain more in detail the issue For point 3, it's a good point I'll look into it |
I'm trying to make an input that lets users input a number between a minimum and maximum range, with a maximum precision. For example, a duration that can be between 0 and 10 seconds, with intervals of 0.1 seconds.
The problem with just using
number_input
with anf32
is the usual floating point issues - it can't represent certain fractions cleanly and you get long rounding errors.Gif of floating point issues
My attempted solution was to create a new type that better represents my business logic. Instead of storing the value as a floating point number of seconds, I store it as an integer number of 10ths of a second. When it comes to display, I divide the internal number by 10 to convert it from 10ths of a second back to whole seconds.
This displays the number very clearly, but the problem now is that the logic handling keyboard input seems to break. In the below gif, the first half is me scrolling nicely through the range, then in the second half I start trying to enter a value using the keyboard. As soon as a single character is entered, it gets placed at the end of the input, and you can't enter any more values.
Gif of issues entering data when using a custom type
Here's the full code from the `number_input` example in the repo which I have edited for the above demonstration:
The text was updated successfully, but these errors were encountered: