Skip to content

Typing a number into the number_input doesn't work properly when using a custom number type to handle formatting precision #319

@mfreeborn

Description

@mfreeborn

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
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions