Skip to content
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

Open
mfreeborn opened this issue Feb 3, 2025 · 8 comments

Comments

@mfreeborn
Copy link

mfreeborn commented Feb 3, 2025

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 an f32 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:
// This example demonstrates how to use the number input widget
//
// It was written by leang27 <[email protected]>

use iced::{
    widget::{Container, Row, Text},
    Alignment, Element, Length,
};
use iced_aw::number_input;

use num_traits::Bounded;

#[derive(Default, Debug)]
pub struct NumberInputDemo {
    value: Duration,
}

#[derive(Debug, Clone)]
pub enum Message {
    NumInpChanged(Duration),
    NumInpSubmitted,
}

fn main() -> iced::Result {
    iced::application(
        "Number Input example",
        NumberInputDemo::update,
        NumberInputDemo::view,
    )
    .window_size(iced::Size {
        width: 250.0,
        height: 200.0,
    })
    .font(iced_fonts::REQUIRED_FONT_BYTES)
    .run()
}

impl NumberInputDemo {
    fn update(&mut self, message: self::Message) {
        match message {
            Message::NumInpChanged(val) => {
                println!("Value changed to {:?}", val);
                self.value = val;
            }
            Message::NumInpSubmitted => {
                println!("Value submitted");
            }
        }
    }

    fn view(&self) -> Element<Message> {
        let lb_minute = Text::new("Number Input:");
        let txt_minute = number_input(
            &self.value,
            Duration::min_value()..=Duration::max_value(),
            Message::NumInpChanged,
        )
        .style(number_input::number_input::primary)
        .on_submit(Message::NumInpSubmitted)
        .step(Duration::new(1).unwrap());

        Container::new(
            Row::new()
                .spacing(10)
                .align_y(Alignment::Center)
                .push(lb_minute)
                .push(txt_minute),
        )
        .width(Length::Fill)
        .height(Length::Fill)
        .center_x(Length::Fill)
        .center_y(Length::Fill)
        .into()
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Default, PartialOrd)]
pub struct Duration(i16);

impl Duration {
    pub fn new(gain: i16) -> 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 = i16;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl num_traits::Num for Duration {
    type FromStrRadixErr = <i16 as num_traits::Num>::FromStrRadixErr;
    fn from_str_radix(str: &str, radix: u32) -> Result<Self, Self::FromStrRadixErr> {
        <i16 as num_traits::Num>::from_str_radix(str, radix).map(Self)
    }
}
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 = <i16 as std::str::FromStr>::Err;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        <i16 as std::str::FromStr>::from_str(s).map(Self)
    }
}
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(1)
    }
    fn is_one(&self) -> bool
    where
        Self: PartialEq,
    {
        self.0 == 1
    }
}
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
    }
}
@genusistimelord
Copy link
Collaborator

genusistimelord commented Feb 3, 2025

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.

@mfreeborn
Copy link
Author

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.

@genusistimelord
Copy link
Collaborator

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.

@Ultraxime
Copy link
Collaborator

Hello,

I think it comes from several mistakes in your code:
The from str trait should deserialise a float an then multiply it by 10 to obtain the i16, because when you format you type you do it as a float

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
If it's not the case I'll have to redo the logic of this widget one more time.

@mfreeborn
Copy link
Author

mfreeborn commented Feb 4, 2025

Hello,

I think it comes from several mistakes in your code: The from str trait should deserialise a float an then multiply it by 10 to obtain the i16, because when you format you type you do it as a float

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 If it's not the case I'll have to redo the logic of this widget one more time.

That gets a lot closer to the desired functionality - both your points about my implementation were valid.

A few outstanding issues:

  1. The input keeps on displaying digits beyond the maximum described precision, whilst the the on_change message isn't emitted. ( I think it's correct that the message isn't emitted, but the input widget now displays an invalid value, which is out of sync with the state that is storing the value.)

    Extra trailing digits
  2. "Sometimes", I can highlight the text in the number input, start typing a value, e.g. 0.5, and it will work correctly. The rest of the time, it won't let me overwrite the value that is already present. I can't get it to trigger all the time, but if I scroll/fiddle with the input a few times then it lets me do it.

    Finally manage to type "8.6" at the end of the gif
  3. Also "sometimes", I can highlight the text in the number input, press backspace to clear it, and then move the focus away from the number input such that the value is just blank. No message gets emitted for this, so the widget is left in an invalid state of containing no value at all

    Leaving the input in an empty, invalid state
Full code used in the above examples
use 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()
}

@Ultraxime
Copy link
Collaborator

Hello,
I think it comes from several mistakes in your code: The from str trait should deserialise a float an then multiply it by 10 to obtain the i16, because when you format you type you do it as a float
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 If it's not the case I'll have to redo the logic of this widget one more time.

That gets a lot closer to the desired functionality - both your points about my implementation were valid.

A few outstanding issues:

  1. The input keeps on displaying digits beyond the maximum described precision, whilst the the on_change message isn't emitted. ( I think it's correct that the message isn't emitted, but the input widget now displays an invalid value, which is out of sync with the state that is storing the value.)

    Extra trailing digits

        [
          
            ![409573422-bdaaaf4e-725e-45f5-9bc3-98432423cc9b.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg3MzE4NTMsIm5iZiI6MTczODczMTU1MywicGF0aCI6Ii8zMTgwNjgwOC80MDk1NzM0MjItYmRhYWFmNGUtNzI1ZS00NWY1LTliYzMtOTg0MzI0MjNjYzliLmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMDUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjA1VDA0NTkxM1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPThjZDQ1OWQ0YTE1NWFhNjM0Njk1ZTNmODUyZTU4NzkzYjQ0NDVmZGIzYmI2NGExZjMzNDc5ODMwOTI2NzcwOTkmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.nPvoPxQ6mcCbXBq1eoc8IA22CUkCiDntt4VD0HBVP_4](https://private-user-images.githubusercontent.com/31806808/409573422-bdaaaf4e-725e-45f5-9bc3-98432423cc9b.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg3MzE4NTMsIm5iZiI6MTczODczMTU1MywicGF0aCI6Ii8zMTgwNjgwOC80MDk1NzM0MjItYmRhYWFmNGUtNzI1ZS00NWY1LTliYzMtOTg0MzI0MjNjYzliLmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMDUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjA1VDA0NTkxM1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPThjZDQ1OWQ0YTE1NWFhNjM0Njk1ZTNmODUyZTU4NzkzYjQ0NDVmZGIzYmI2NGExZjMzNDc5ODMwOTI2NzcwOTkmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.nPvoPxQ6mcCbXBq1eoc8IA22CUkCiDntt4VD0HBVP_4)
          
        ](https://private-user-images.githubusercontent.com/31806808/409573422-bdaaaf4e-725e-45f5-9bc3-98432423cc9b.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg3MzE4NTMsIm5iZiI6MTczODczMTU1MywicGF0aCI6Ii8zMTgwNjgwOC80MDk1NzM0MjItYmRhYWFmNGUtNzI1ZS00NWY1LTliYzMtOTg0MzI0MjNjYzliLmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMDUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjA1VDA0NTkxM1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPThjZDQ1OWQ0YTE1NWFhNjM0Njk1ZTNmODUyZTU4NzkzYjQ0NDVmZGIzYmI2NGExZjMzNDc5ODMwOTI2NzcwOTkmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.nPvoPxQ6mcCbXBq1eoc8IA22CUkCiDntt4VD0HBVP_4)
        
        
          
            
              
            
            
              
              
            
          
          [
            
              
            
          ](https://private-user-images.githubusercontent.com/31806808/409573422-bdaaaf4e-725e-45f5-9bc3-98432423cc9b.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg3MzE4NTMsIm5iZiI6MTczODczMTU1MywicGF0aCI6Ii8zMTgwNjgwOC80MDk1NzM0MjItYmRhYWFmNGUtNzI1ZS00NWY1LTliYzMtOTg0MzI0MjNjYzliLmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMDUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjA1VDA0NTkxM1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPThjZDQ1OWQ0YTE1NWFhNjM0Njk1ZTNmODUyZTU4NzkzYjQ0NDVmZGIzYmI2NGExZjMzNDc5ODMwOTI2NzcwOTkmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.nPvoPxQ6mcCbXBq1eoc8IA22CUkCiDntt4VD0HBVP_4)
    
  2. "Sometimes", I can highlight the text in the number input, start typing a value, e.g. 0.5, and it will work correctly. The rest of the time, it won't let me overwrite the value that is already present. I can't get it to trigger all the time, but if I scroll/fiddle with the input a few times then it lets me do it.

    Finally manage to type "8.6" at the end of the gif

        [
          
        
            ![409573149-6512b228-b87b-4ffa-b097-7c758d9dc3ff.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg3MzE4NTMsIm5iZiI6MTczODczMTU1MywicGF0aCI6Ii8zMTgwNjgwOC80MDk1NzMxNDktNjUxMmIyMjgtYjg3Yi00ZmZhLWIwOTctN2M3NThkOWRjM2ZmLmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMDUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjA1VDA0NTkxM1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWMwNzZkMzc5YmQzODg0MDU5MjMyN2IxMWZlNjA1ZTVjYjkzNzJhODhhYzI4Yzc2OGZlYmU2MTBjOTNlMzZiMDAmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.SUh9K3jmwatzdVmI63ByO1THSiFBhjltZbD-AcaZlME](https://private-user-images.githubusercontent.com/31806808/409573149-6512b228-b87b-4ffa-b097-7c758d9dc3ff.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg3MzE4NTMsIm5iZiI6MTczODczMTU1MywicGF0aCI6Ii8zMTgwNjgwOC80MDk1NzMxNDktNjUxMmIyMjgtYjg3Yi00ZmZhLWIwOTctN2M3NThkOWRjM2ZmLmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMDUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjA1VDA0NTkxM1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWMwNzZkMzc5YmQzODg0MDU5MjMyN2IxMWZlNjA1ZTVjYjkzNzJhODhhYzI4Yzc2OGZlYmU2MTBjOTNlMzZiMDAmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.SUh9K3jmwatzdVmI63ByO1THSiFBhjltZbD-AcaZlME)
          ](https://private-user-images.githubusercontent.com/31806808/409573149-6512b228-b87b-4ffa-b097-7c758d9dc3ff.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg3MzE4NTMsIm5iZiI6MTczODczMTU1MywicGF0aCI6Ii8zMTgwNjgwOC80MDk1NzMxNDktNjUxMmIyMjgtYjg3Yi00ZmZhLWIwOTctN2M3NThkOWRjM2ZmLmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMDUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjA1VDA0NTkxM1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWMwNzZkMzc5YmQzODg0MDU5MjMyN2IxMWZlNjA1ZTVjYjkzNzJhODhhYzI4Yzc2OGZlYmU2MTBjOTNlMzZiMDAmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.SUh9K3jmwatzdVmI63ByO1THSiFBhjltZbD-AcaZlME)
        
        
          
            
              
            
            
              
              
            
          
          [
            
              
            
          ](https://private-user-images.githubusercontent.com/31806808/409573149-6512b228-b87b-4ffa-b097-7c758d9dc3ff.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg3MzE4NTMsIm5iZiI6MTczODczMTU1MywicGF0aCI6Ii8zMTgwNjgwOC80MDk1NzMxNDktNjUxMmIyMjgtYjg3Yi00ZmZhLWIwOTctN2M3NThkOWRjM2ZmLmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMDUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjA1VDA0NTkxM1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWMwNzZkMzc5YmQzODg0MDU5MjMyN2IxMWZlNjA1ZTVjYjkzNzJhODhhYzI4Yzc2OGZlYmU2MTBjOTNlMzZiMDAmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.SUh9K3jmwatzdVmI63ByO1THSiFBhjltZbD-AcaZlME)
    
  3. Also "sometimes", I can highlight the text in the number input, press backspace to clear it, and then move the focus away from the number input such that the value is just blank. No message gets emitted for this, so the widget is left in an invalid state of containing no value at all

    Leaving the input in an empty, invalid state

        [
          
            ![409573888-98249241-4c6f-4432-b85c-4d104b1f8c34.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg3MzE4NTMsIm5iZiI6MTczODczMTU1MywicGF0aCI6Ii8zMTgwNjgwOC80MDk1NzM4ODgtOTgyNDkyNDEtNGM2Zi00NDMyLWI4NWMtNGQxMDRiMWY4YzM0LmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMDUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjA1VDA0NTkxM1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTczMGUxNDUwMTA3ZWJjY2ZhMzQxZmM4ODNiNjNiNzQyYmRkNmQ2ZTU0YTI3M2Q5NzQ3ZmZlMWQ2MjhhNDkyYmImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.Ssq6R9AONaxS86flFZFCR0mdL1GGAsIV7-5CJw8rIgY](https://private-user-images.githubusercontent.com/31806808/409573888-98249241-4c6f-4432-b85c-4d104b1f8c34.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg3MzE4NTMsIm5iZiI6MTczODczMTU1MywicGF0aCI6Ii8zMTgwNjgwOC80MDk1NzM4ODgtOTgyNDkyNDEtNGM2Zi00NDMyLWI4NWMtNGQxMDRiMWY4YzM0LmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMDUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjA1VDA0NTkxM1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTczMGUxNDUwMTA3ZWJjY2ZhMzQxZmM4ODNiNjNiNzQyYmRkNmQ2ZTU0YTI3M2Q5NzQ3ZmZlMWQ2MjhhNDkyYmImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.Ssq6R9AONaxS86flFZFCR0mdL1GGAsIV7-5CJw8rIgY)
          
        ](https://private-user-images.githubusercontent.com/31806808/409573888-98249241-4c6f-4432-b85c-4d104b1f8c34.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg3MzE4NTMsIm5iZiI6MTczODczMTU1MywicGF0aCI6Ii8zMTgwNjgwOC80MDk1NzM4ODgtOTgyNDkyNDEtNGM2Zi00NDMyLWI4NWMtNGQxMDRiMWY4YzM0LmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMDUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjA1VDA0NTkxM1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTczMGUxNDUwMTA3ZWJjY2ZhMzQxZmM4ODNiNjNiNzQyYmRkNmQ2ZTU0YTI3M2Q5NzQ3ZmZlMWQ2MjhhNDkyYmImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.Ssq6R9AONaxS86flFZFCR0mdL1GGAsIV7-5CJw8rIgY)
        
        
          
            
              
            
            
              
              
            
          
          [
            
              
            
          ](https://private-user-images.githubusercontent.com/31806808/409573888-98249241-4c6f-4432-b85c-4d104b1f8c34.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg3MzE4NTMsIm5iZiI6MTczODczMTU1MywicGF0aCI6Ii8zMTgwNjgwOC80MDk1NzM4ODgtOTgyNDkyNDEtNGM2Zi00NDMyLWI4NWMtNGQxMDRiMWY4YzM0LmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMDUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjA1VDA0NTkxM1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTczMGUxNDUwMTA3ZWJjY2ZhMzQxZmM4ODNiNjNiNzQyYmRkNmQ2ZTU0YTI3M2Q5NzQ3ZmZlMWQ2MjhhNDkyYmImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.Ssq6R9AONaxS86flFZFCR0mdL1GGAsIV7-5CJw8rIgY)
    

Full code used in the above examples

  1. The issue is that 2.34 is something valid for an f32 so when you deserialise it is ok and your type doesn't emit an error. A solution would be in your try from trait to check if multiplied by 10 the float is an integer or not, and throw an error if it's not

  2. Not sure I understood this point, will look into it to understand

  3. That's an "intended" behaviour. Because I don't know how to prevent you from changing the focus (and don't think it's a good idea) and it prevents writing something in it that the user would not like. So currently if you unfocus the widget with an invalid value it just remembers the last valid one, or maybe I did it in a way that "" is treated as T::zero.

Hope it helps

@mfreeborn
Copy link
Author

1. The issue is that 2.34 is something valid for an f32 so when you deserialise it is ok and your type doesn't emit an error. A solution would be in your try from trait to check if multiplied by 10 the float is an integer or not, and throw an error if it's not

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),
        }
    }
}
2. Not sure I understood this point, will look into it to understand

In short, it's not letting me type in valid values.

3. That's an "intended" behaviour. Because I don't know how to prevent you from changing the focus (and don't think it's a good idea) and it prevents writing something in it that the user would not like. So currently if you unfocus the widget with an invalid value it just remembers the last valid one, or maybe I did it in a way that "" is treated as T::zero.

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!)

Hope it helps

Absolutely, thanks for your help so far :)

@Ultraxime
Copy link
Collaborator

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants