yewprint 0.5.0

Port of blueprintjs.com to Yew
Documentation
use crate::{Button, ButtonGroup, ControlGroup, Icon, InputGroup, Intent};
use std::fmt::Display;
use std::marker::PhantomData;
use std::ops::{Add, Bound, RangeBounds, Sub};
use std::str::FromStr;
use web_sys::HtmlInputElement;
use yew::html::IntoPropValue;
use yew::prelude::*;

#[derive(Debug)]
pub struct NumericInput<T>
where
    T: Add<Output = T>
        + Sub<Output = T>
        + Copy
        + Display
        + FromStr
        + PartialEq
        + PartialOrd
        + 'static,
{
    input: String,
    phantom: PhantomData<T>,
}

#[derive(Clone, PartialEq, Properties)]
pub struct NumericInputProps<T>
where
    T: Add<Output = T>
        + Sub<Output = T>
        + Copy
        + Display
        + FromStr
        + PartialEq
        + PartialOrd
        + 'static,
{
    #[prop_or_default]
    pub disabled: bool,
    #[prop_or_default]
    pub fill: bool,
    #[prop_or_default]
    pub large: bool,
    #[prop_or_default]
    pub class: Classes,
    #[prop_or_default]
    pub placeholder: AttrValue,
    #[prop_or_default]
    pub left_icon: Option<Icon>,
    #[prop_or_default]
    pub left_element: Option<Html>,
    #[prop_or_default]
    pub right_element: Option<Html>,
    #[prop_or_default]
    pub intent: Option<Intent>,
    #[prop_or_default]
    pub onchange: Callback<T>,
    pub value: T,
    #[prop_or_default]
    pub bounds: NumericInputRangeBounds<T>,
    pub increment: T,
    #[prop_or_default]
    pub disable_buttons: bool,
    #[prop_or_default]
    pub buttons_on_the_left: bool,
}

pub enum NumericInputMsg {
    InputUpdate(String),
    Up,
    Down,
    Noop,
}

impl<T> Component for NumericInput<T>
where
    T: Add<Output = T>
        + Sub<Output = T>
        + Copy
        + Display
        + FromStr
        + PartialEq
        + PartialOrd
        + 'static,
{
    type Message = NumericInputMsg;
    type Properties = NumericInputProps<T>;

    fn create(_ctx: &Context<Self>) -> Self {
        Self {
            input: Default::default(),
            phantom: PhantomData,
        }
    }

    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
        let mut update_value = |new_value| {
            let new_value = ctx.props().bounds.clamp(new_value, ctx.props().increment);

            if new_value != ctx.props().value {
                self.input = new_value.to_string();
                ctx.props().onchange.emit(new_value);
                true
            } else {
                false
            }
        };

        match msg {
            NumericInputMsg::InputUpdate(new_value) => {
                if let Ok(new_value) = new_value.trim().parse::<T>() {
                    update_value(new_value)
                } else {
                    false
                }
            }
            NumericInputMsg::Up => update_value(ctx.props().value + ctx.props().increment),
            NumericInputMsg::Down => update_value(ctx.props().value - ctx.props().increment),
            NumericInputMsg::Noop => false,
        }
    }

    fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
        self.input = ctx.props().value.to_string();
        true
    }

    fn view(&self, ctx: &Context<Self>) -> Html {
        let NumericInputProps {
            value,
            increment,
            disabled,
            disable_buttons,
            buttons_on_the_left,
            large,
            placeholder,
            left_icon,
            left_element,
            right_element,
            fill,
            bounds,
            class,
            intent,
            onchange: _,
        } = &ctx.props();

        let button_up_disabled =
            *disabled || bounds.clamp(*value + *increment, *increment) == *value;
        let button_down_disabled =
            *disabled || bounds.clamp(*value - *increment, *increment) == *value;

        let buttons = if *disable_buttons {
            html!()
        } else {
            html! {
                <ButtonGroup vertical=true class={classes!("bp3-fixed")}>
                    <Button
                        icon={Icon::ChevronUp}
                        disabled={button_up_disabled}
                        onclick={ctx.link().callback(|_| NumericInputMsg::Up)}
                        {intent}
                    />
                    <Button
                        icon={Icon::ChevronDown}
                        disabled={button_down_disabled}
                        onclick={ctx.link().callback(|_| NumericInputMsg::Down)}
                        {intent}
                    />
                </ButtonGroup>
            }
        };

        let input_group = html! {
            <InputGroup
                {placeholder}
                {large}
                {disabled}
                {left_icon}
                left_element={left_element.clone()}
                right_element={right_element.clone()}
                value={self.input.clone()}
                oninput={ctx.link().callback(|e: InputEvent| {
                    let value = e.target_unchecked_into::<HtmlInputElement>().value();
                    NumericInputMsg::InputUpdate(value)
                })}
                onkeydown={ctx.link().callback(|e: KeyboardEvent| {
                    if e.key() == "ArrowUp" {
                        NumericInputMsg::Up
                    } else if e.key() == "ArrowDown" {
                        NumericInputMsg::Down
                    } else {
                        NumericInputMsg::Noop
                    }
                })}
            />
        };

        if *buttons_on_the_left {
            html! {
                <ControlGroup
                    class={classes!("bp3-numeric-input", class.clone())}
                    {fill}
                    {large}
                >
                    {buttons}
                    {input_group}
                </ControlGroup>
            }
        } else {
            html! {
                <ControlGroup
                    class={classes!("bp3-numeric-input", class.clone())}
                    {fill}
                    {large}
                >
                    {input_group}
                    {buttons}
                </ControlGroup>
            }
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NumericInputRangeBounds<T> {
    pub start: Bound<T>,
    pub end: Bound<T>,
}

impl<T> Default for NumericInputRangeBounds<T> {
    fn default() -> Self {
        Self {
            start: Bound::Unbounded,
            end: Bound::Unbounded,
        }
    }
}

impl<T> NumericInputRangeBounds<T>
where
    T: PartialOrd + Copy + Add<Output = T> + Sub<Output = T>,
{
    pub fn clamp(&self, value: T, increment: T) -> T {
        match (self.start, self.end) {
            (Bound::Included(min), _) if value < min => min,
            (Bound::Excluded(min), _) if value <= min => min + increment,
            (_, Bound::Included(max)) if value > max => max,
            (_, Bound::Excluded(max)) if value >= max => max - increment,
            _ => value,
        }
    }
}

macro_rules! impl_into_prop_value {
    ($path:path) => {
        impl<T: Copy> IntoPropValue<NumericInputRangeBounds<T>> for $path {
            fn into_prop_value(self) -> NumericInputRangeBounds<T> {
                NumericInputRangeBounds {
                    start: self.start_bound().cloned(),
                    end: self.end_bound().cloned(),
                }
            }
        }
    };
}

impl_into_prop_value!(std::ops::Range<T>);
impl_into_prop_value!(std::ops::RangeFrom<T>);
impl_into_prop_value!(std::ops::RangeFull);
impl_into_prop_value!(std::ops::RangeInclusive<T>);
impl_into_prop_value!(std::ops::RangeTo<T>);
impl_into_prop_value!(std::ops::RangeToInclusive<T>);