Implementing a custom struct with uses several other built-in widgets internally

My head is going around in circles, so hopefully I can get some pointers.

What I want to build is a custom widget like this:

image

In my mind, the CustomWidget struct directly owns the child widgets, e.g.:

struct CustomWidget {
    text_input: TextInput<...>,
    up_button: Button<...>,
    down_button: Button<...>
}

impl CustomWidget {
    pub fn new(...) -> Self {
        
        Self {
            text_input: TextInput::new(...),
            up_button: button(text("up icon")),
            down_button: button(text("down_icon"))
        }
    }
}

However, I am struggling with mismatched types here, there and everywhere, particularly when I use text as the button content, rather than just a simple string.

Before going too much further, is that the correct structure to even start with?

I’ve already looked at/modified this widget, which wraps just the text_input: iced_aw/src/widget/number_input.rs at main Ā· iced-rs/iced_aw Ā· GitHub

I’m just experimenting to see if it’s a button more logical to use actual button widgets directly, rather than mocking them as containers with text and (re)-implementing button logic on top of that.

Such a widget also exists in iced itself, you can use that for reference as well: iced/widget/src/combo_box.rs at master Ā· iced-rs/iced Ā· GitHub

I’d question whether this needs to be a Widget at all

You can write a number.rs module that has a view function made up of row[input, column[up, down]] with its own Message and update function, and reuse it across your app.

To make it easier on you, write fn view and fn update as standalone functions, instead of methods to some struct. Pass the input string as a borrowed reference to both functions.

1 Like

I looked at that example in detail and built up my implementation from scratch - I think I had originally missed a trait bound/generic somewhere and there were too many things going on at once for me to isolate it.

Anyway, it turned out to be quite involved and very gnarly in places.

use std::{cell::RefCell, cmp::Ordering};

use iced::{
    Alignment, Element, Event, Font, Length, Padding, Rectangle, Size,
    advanced::{
        Layout, Shell, Widget,
        layout::{Limits, Node},
        mouse, renderer, text as advanced_text,
        widget::{self, Tree, tree::Tag},
    },
    keyboard::{self, Key, key::Named},
    mouse::ScrollDelta,
    widget::{
        Button, TextInput, button, text,
        text_input::{self, Value},
    },
};
use rust_decimal::Decimal;

#[derive(Debug, Clone)]
enum TextEvent {
    TextChanged(String),
}

#[derive(Debug, Clone)]
enum ButtonEvent {
    IncrementButtonPressed,
    DecrementButtonPressed,
}

pub struct DecimalInput2<'a, Decimal, Message, Theme = iced::Theme, Renderer = iced::Renderer>
where
    Decimal: NumberInput,
    Theme: Catalog,
    Renderer: advanced_text::Renderer,
{
    content: &'a Content<Decimal>,
    text_input: TextInput<'a, TextEvent, Theme, Renderer>,
    increment_button: Button<'a, ButtonEvent, Theme, Renderer>,
    decrement_button: Button<'a, ButtonEvent, Theme, Renderer>,
    value: text_input::Value,
    on_change: Box<dyn Fn(Decimal) -> Message>,
}

impl<'a, Decimal, Message, Theme, Renderer> DecimalInput2<'a, Decimal, Message, Theme, Renderer>
where
    Decimal: NumberInput,
    Theme: Catalog + widget::text::Catalog + 'a,
    Renderer: advanced_text::Renderer + 'a,
    <Renderer as iced::advanced::text::Renderer>::Font: From<iced::Font>,
{
    pub fn new(
        content: &'a Content<Decimal>,
        value: &Decimal,
        on_change: impl Fn(Decimal) -> Message + 'static,
    ) -> Self {
        let text_input = TextInput::new("", &content.value())
            .on_input(TextEvent::TextChanged)
            .width(Length::Fill)
            .class(Theme::default_input());

        let increment_button = button(
            text("\u{25B4}")
                .font(Font::with_name("icons"))
                .size(15)
                .center(),
        )
        .on_press_maybe(
            (<Decimal as NumberInput>::can_increment(value))
                .then_some(ButtonEvent::IncrementButtonPressed),
        )
        .padding(Padding::ZERO.left(3).right(3))
        .class(Theme::default_button());

        let decrement_button = button(
            text("\u{25BE}")
                .font(Font::with_name("icons"))
                .size(15)
                .center(),
        )
        .on_press_maybe(
            (<Decimal as NumberInput>::can_decrement(value))
                .then_some(ButtonEvent::DecrementButtonPressed),
        )
        .padding(Padding::ZERO.left(3).right(3))
        .class(Theme::default_button());

        let value = value.to_string();

        Self {
            content,
            text_input,
            increment_button,
            decrement_button,
            value: text_input::Value::new(&value),
            on_change: Box::new(on_change),
        }
    }

    /// Sets the style of the input of the [`DecimalInput2`].
    #[must_use]
    pub fn input_style(
        mut self,
        style: impl Fn(&Theme, text_input::Status) -> text_input::Style + 'a,
    ) -> Self
    where
        <Theme as text_input::Catalog>::Class<'a>: From<text_input::StyleFn<'a, Theme>>,
    {
        self.text_input = self.text_input.style(style);
        self
    }

    #[must_use]
    pub fn input_class(
        mut self,
        class: impl Into<<Theme as text_input::Catalog>::Class<'a>>,
    ) -> Self {
        self.text_input = self.text_input.class(class);
        self
    }

    /// Sets the style of the input of the [`DecimalInput2`].
    #[must_use]
    pub fn button_style(
        mut self,
        increment_style: impl Fn(&Theme, button::Status) -> button::Style + 'a,
        decrement_style: impl Fn(&Theme, button::Status) -> button::Style + 'a,
    ) -> Self
    where
        <Theme as button::Catalog>::Class<'a>: From<button::StyleFn<'a, Theme>>,
    {
        self.increment_button = self.increment_button.style(increment_style);
        self.decrement_button = self.decrement_button.style(decrement_style);
        self
    }

    #[must_use]
    pub fn button_class(
        mut self,
        incremenet_class: impl Into<<Theme as button::Catalog>::Class<'a>>,
        decremenet_class: impl Into<<Theme as button::Catalog>::Class<'a>>,
    ) -> Self {
        self.increment_button = self.increment_button.class(incremenet_class);
        self.decrement_button = self.decrement_button.class(decremenet_class);
        self
    }

    fn do_increment(&self, shell: &mut Shell<'_, Message>, step_size: &StepSize) {
        self.content.with_inner_mut(|state| {
            let current_value = &state.last_valid_value;
            let new_value = Decimal::increment(current_value, step_size);

            state.last_valid_value = new_value.clone();
            state.value = state.last_valid_value.to_string();

            shell.publish((self.on_change)(new_value));
        });
    }

    fn do_decrement(&self, shell: &mut Shell<'_, Message>, step_size: &StepSize) {
        self.content.with_inner_mut(|state| {
            let current_value = &state.last_valid_value;
            let new_value = Decimal::decrement(current_value, step_size);

            state.last_valid_value = new_value.clone();
            state.value = state.last_valid_value.to_string();

            shell.publish((self.on_change)(new_value));
        });
    }
}

impl<'a, Decimal, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
    for DecimalInput2<'a, Decimal, Message, Theme, Renderer>
where
    Decimal: NumberInput,
    Message: Clone + 'a,
    Theme: Catalog + button::Catalog + widget::text::Catalog + 'a,
    Renderer: advanced_text::Renderer + 'a,
    <Renderer as advanced_text::Renderer>::Font: From<Font>,
{
    fn size(&self) -> Size<Length> {
        Widget::<TextEvent, Theme, Renderer>::size(&self.text_input)
    }

    fn tag(&self) -> Tag {
        Tag::of::<State>()
    }

    fn state(&self) -> widget::tree::State {
        widget::tree::State::new(State::default())
    }

    fn children(&self) -> Vec<Tree> {
        vec![
            Tree::new(&self.text_input as &dyn Widget<_, _, _>),
            Tree::new(&self.increment_button as &dyn Widget<_, _, _>),
            Tree::new(&self.decrement_button as &dyn Widget<_, _, _>),
        ]
    }

    fn diff(&self, _tree: &mut Tree) {
        // do nothing so the children don't get cleared
    }

    fn layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
        let is_focused = {
            let text_input_state = tree.children[0]
                .state
                .downcast_ref::<text_input::State<Renderer::Paragraph>>();

            text_input_state.is_focused()
        };

        let text_input_node = self.text_input.layout(
            &mut tree.children[0],
            renderer,
            limits,
            (!is_focused).then_some(&self.value),
        );
        // if we want to add spacing, we might need to shrink the font size, lest the buttons shrink to nothing
        let button_spacing = 0.0;
        let limits = limits.height((text_input_node.size().height / 2.0) - button_spacing / 2.0);

        let increment_button_node = self
            .increment_button
            .layout(&mut tree.children[1], renderer, &limits)
            .align(Alignment::End, Alignment::Start, text_input_node.size());

        let decrement_button_node = self
            .decrement_button
            .layout(&mut tree.children[2], renderer, &limits)
            .align(Alignment::End, Alignment::End, text_input_node.size());

        Node::with_children(
            text_input_node.size(),
            vec![
                text_input_node,
                increment_button_node,
                decrement_button_node,
            ],
        )
    }

    fn draw(
        &self,
        tree: &Tree,
        renderer: &mut Renderer,
        theme: &Theme,
        style: &renderer::Style,
        layout: Layout<'_>,
        cursor: mouse::Cursor,
        viewport: &iced::Rectangle,
    ) {
        self.text_input.draw(
            &tree.children[0],
            renderer,
            theme,
            layout.children().nth(0).unwrap(),
            cursor,
            None,
            viewport,
        );

        self.increment_button.draw(
            &tree.children[1],
            renderer,
            theme,
            style,
            layout.children().nth(1).unwrap(),
            cursor,
            viewport,
        );

        self.decrement_button.draw(
            &tree.children[2],
            renderer,
            theme,
            style,
            layout.children().nth(2).unwrap(),
            cursor,
            viewport,
        );
    }

    fn update(
        &mut self,
        tree: &mut Tree,
        event: &iced::Event,
        layout: Layout<'_>,
        cursor: mouse::Cursor,
        renderer: &Renderer,
        clipboard: &mut dyn iced::advanced::Clipboard,
        shell: &mut Shell<'_, Message>,
        viewport: &Rectangle,
    ) {
        let state = tree.state.downcast_mut::<State>();

        if let &Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) = event {
            state.keyboard_modifiers = modifiers;
        }

        // Handle unfocus events; return early if so
        let now_focused = {
            let text_input_state = tree.children[0]
                .state
                .downcast_ref::<text_input::State<Renderer::Paragraph>>();

            text_input_state.is_focused()
        };

        let was_focused = state.is_focused;
        state.is_focused = now_focused;
        if !now_focused && was_focused {
            let current_text = self.content.value();
            let new = Decimal::resolve(&current_text);

            self.content.with_inner_mut(|state| {
                state.last_valid_value = new.clone();
                state.value = state.last_valid_value.to_string();
            });
            shell.publish((self.on_change)(new));
            return;
        }

        // Handle the buttons being clicked; return early if so
        // Create a new list of local messages
        let mut local_messages = Vec::new();
        let mut local_shell = Shell::new(&mut local_messages);

        // Provide it to the widget
        self.increment_button.update(
            &mut tree.children[1],
            event,
            layout.children().nth(1).unwrap(),
            cursor,
            renderer,
            clipboard,
            &mut local_shell,
            viewport,
        );

        self.decrement_button.update(
            &mut tree.children[2],
            event,
            layout.children().nth(2).unwrap(),
            cursor,
            renderer,
            clipboard,
            &mut local_shell,
            viewport,
        );

        if local_shell.is_event_captured() {
            shell.capture_event();

            shell.request_redraw_at(local_shell.redraw_request());

            for m in local_messages {
                match m {
                    ButtonEvent::IncrementButtonPressed => {
                        self.do_increment(shell, &StepSize::Normal);
                    }
                    ButtonEvent::DecrementButtonPressed => {
                        self.do_decrement(shell, &StepSize::Normal);
                    }
                }
            }
            let text_input_state = tree.children[0]
                .state
                .downcast_mut::<text_input::State<Renderer::Paragraph>>();
            text_input_state.unfocus();
            state.is_focused = false;
            return;
        }

        // Handle the scroll wheel input next; return early if so
        if now_focused || cursor.is_over(layout.bounds()) {
            if let &Event::Mouse(mouse::Event::WheelScrolled { delta }) = event {
                let step_size = match state.keyboard_modifiers.command() {
                    true => StepSize::Large,
                    false => StepSize::Normal,
                };

                match delta {
                    ScrollDelta::Lines { y, .. } | ScrollDelta::Pixels { y, .. } => {
                        if y.is_sign_positive() {
                            self.do_increment(shell, &step_size);
                            shell.capture_event();
                        } else {
                            self.do_decrement(shell, &step_size);
                            shell.capture_event();
                        }
                    }
                }
                return;
            }
        }

        // Handle Arrow{Up|Down} and Page{Up|Down} and Enter; return early if so
        if now_focused {
            if let &Event::Keyboard(keyboard::Event::KeyPressed {
                key: keyboard::Key::Named(named),
                text,
                ..
            }) = &event
            {
                match (named, text) {
                    (keyboard::key::Named::ArrowUp, None) => {
                        return self.do_increment(shell, &StepSize::Normal);
                    }
                    (keyboard::key::Named::PageUp, None) => {
                        return self.do_increment(shell, &StepSize::Large);
                    }
                    (keyboard::key::Named::ArrowDown, None) => {
                        return self.do_decrement(shell, &StepSize::Normal);
                    }
                    (keyboard::key::Named::PageDown, None) => {
                        return self.do_decrement(shell, &StepSize::Large);
                    }
                    (keyboard::key::Named::Enter, _) => {
                        let text_input_state = tree.children[0]
                            .state
                            .downcast_mut::<text_input::State<Renderer::Paragraph>>();
                        // Convert a submit event into an unfocus event
                        return text_input_state.unfocus();
                    }
                    _ => (),
                };
            }
        }

        let event = event.clone();
        let event = match event {
            Event::Keyboard(ref kb_event) => match kb_event {
                keyboard::Event::KeyPressed {
                    modified_key,
                    physical_key,
                    location,
                    modifiers,
                    text: Some(text),
                    ..
                } if text.chars().any(|c| {
                    c.is_ascii_digit() || c == '.' || c == '-' || c == '\u{7f}' || c == '\u{8}'
                }) =>
                {
                    let text_input_state = tree.children[0]
                        .state
                        .downcast_ref::<text_input::State<Renderer::Paragraph>>();
                    let cursor = text_input_state.cursor();

                    let new_char = text.chars().next().unwrap();
                    let is_backspace = new_char == '\u{8}';
                    let is_delete = new_char == '\u{7f}';
                    let new_char = text
                        .chars()
                        .find(|c| c.is_ascii_digit() || *c == '.' || *c == '-');

                    let current_content = self.content.value();
                    let mut next_content = current_content.clone();

                    let current_cursor_idx = match cursor.state(&Value::new(&current_content)) {
                        text_input::cursor::State::Index(idx) => {
                            if is_backspace && idx > 0 {
                                next_content.remove(idx - 1);
                            } else if is_delete && idx < current_content.len() {
                                next_content.remove(idx);
                            } else if let Some(c) = new_char {
                                next_content.insert(idx, c);
                            }

                            idx
                        }
                        text_input::cursor::State::Selection { start, end } => {
                            let (start, end) = if start > end {
                                (end, start)
                            } else {
                                (start, end)
                            };

                            next_content.drain(start..end);

                            if let Some(c) = new_char {
                                next_content.insert(start, c);
                            }

                            start
                        }
                    };

                    let next_content = next_content;
                    let next_content_valid = Decimal::maybe_valid(&next_content);

                    if next_content_valid {
                        Some(event)
                    } else if new_char == Some('.') {
                        if Some(current_cursor_idx) == current_content.find('.')
                            && Decimal::precision() > 0
                        {
                            let e = Event::Keyboard(keyboard::Event::KeyPressed {
                                key: keyboard::Key::Named(keyboard::key::Named::ArrowRight),
                                modified_key: modified_key.clone(),
                                physical_key: *physical_key,
                                location: *location,
                                modifiers: *modifiers,
                                text: None,
                            });

                            Some(e)
                        } else {
                            None
                        }
                    } else if new_char == Some('-') {
                        if current_cursor_idx == 0 && current_content.contains('-') {
                            let e = Event::Keyboard(keyboard::Event::KeyPressed {
                                key: keyboard::Key::Named(keyboard::key::Named::ArrowRight),
                                modified_key: modified_key.clone(),
                                physical_key: *physical_key,
                                location: *location,
                                modifiers: *modifiers,
                                text: None,
                            });

                            Some(e)
                        } else {
                            None
                        }
                    } else {
                        None
                    }
                }
                keyboard::Event::KeyPressed {
                    key: Key::Named(named),
                    text: None,
                    ..
                } => match named {
                    Named::ArrowLeft | Named::ArrowRight | Named::Home | Named::End => Some(event),
                    _ => None,
                },
                keyboard::Event::KeyPressed { .. } => None,
                _ => Some(event),
            },
            _ => Some(event),
        };

        if let Some(event) = event {
            // Create a new list of local messages
            let mut local_messages = Vec::new();
            let mut local_shell = Shell::new(&mut local_messages);

            // Provide it to the widget
            self.text_input.update(
                &mut tree.children[0],
                &event,
                layout,
                cursor,
                renderer,
                clipboard,
                &mut local_shell,
                viewport,
            );

            if local_shell.is_event_captured() {
                shell.capture_event();
            }

            shell.request_redraw_at(local_shell.redraw_request());
            shell.request_input_method(local_shell.input_method());

            for m in local_messages {
                match m {
                    TextEvent::TextChanged(txt) => {
                        self.content.with_inner_mut(|state| {
                            state.value = txt;
                        });
                        shell.invalidate_widgets();
                    }
                }
            }
        }
    }

    fn mouse_interaction(
        &self,
        tree: &widget::Tree,
        layout: Layout<'_>,
        cursor: mouse::Cursor,
        viewport: &Rectangle,
        renderer: &Renderer,
    ) -> mouse::Interaction {
        let incr_button = layout.children().nth(1).unwrap().bounds();
        if cursor.is_over(incr_button) {
            return self.increment_button.mouse_interaction(
                &tree.children[1],
                layout,
                cursor,
                viewport,
                renderer,
            );
        }

        let decr_button = layout.children().nth(2).unwrap().bounds();
        if cursor.is_over(decr_button) {
            return self.increment_button.mouse_interaction(
                &tree.children[2],
                layout,
                cursor,
                viewport,
                renderer,
            );
        }
        self.text_input
            .mouse_interaction(&tree.children[0], layout, cursor, viewport, renderer)
    }
}

#[derive(Debug, Clone, Copy, Default)]
struct State {
    is_focused: bool,
    keyboard_modifiers: keyboard::Modifiers,
}

pub trait NumberInput: Clone {
    const DEFAULT: Decimal;
    const MAX: Decimal;
    const MIN: Decimal;

    fn as_decimal(&self) -> &Decimal;

    fn from_decimal(decimal: Decimal) -> Self;

    fn maybe_valid(string: &str) -> bool {
        if let Some(idx) = string.find('-') {
            if Self::MIN.is_sign_positive()
                || idx != 0
                || string.chars().filter(|c| *c == '-').count() > 1
            {
                return false;
            }
        }

        let n_dots = string.chars().filter(|c| *c == '.').count();
        if n_dots > 1 || (Self::precision() == 0 && n_dots > 0) {
            return false;
        }

        let mantissa = match string.split_once('.') {
            Some((integer, fraction)) => {
                if fraction.len() > Self::precision() as usize {
                    return false;
                }

                integer
                    .chars()
                    .chain(fraction.chars().chain(std::iter::repeat_n(
                        '0',
                        Self::precision() as usize - fraction.len(),
                    )))
                    .collect::<String>()
            }
            None => if string.is_empty() || string == "-" {
                "0".to_string()
            } else {
                string.to_string()
            }
            .chars()
            .chain(std::iter::repeat_n('0', Self::precision() as usize))
            .collect::<String>(),
        };

        dbg!(&mantissa);

        if let Ok(mantissa) = mantissa.parse() {
            let decimal = Decimal::new(mantissa, Self::precision());
            match decimal.is_sign_positive() {
                true => decimal <= Self::MAX,
                false => decimal >= Self::MIN,
            }
        } else {
            false
        }
    }

    fn resolve(value: &str) -> Self {
        let value = value.trim();
        if value.is_empty() {
            return Self::from_decimal(Self::DEFAULT);
        }

        let (integer, fraction) = match value.split_once('.') {
            Some((integer, fraction)) => {
                let integer = if integer.is_empty() {
                    "0".to_string()
                } else if integer == "-" {
                    "-0".to_string()
                } else {
                    integer.to_string()
                };

                let fraction = match fraction.len().cmp(&(Self::precision() as usize)) {
                    Ordering::Equal => fraction.to_string(),
                    Ordering::Less => fraction
                        .chars()
                        .chain(std::iter::repeat_n(
                            '0',
                            Self::precision() as usize - fraction.len(),
                        ))
                        .collect::<String>(),
                    Ordering::Greater => fraction
                        .chars()
                        .take(Self::precision() as usize)
                        .collect::<String>(),
                };

                (integer, fraction)
            }
            None => {
                let fraction =
                    std::iter::repeat_n('0', Self::precision() as usize).collect::<String>();
                let integer = if fraction.is_empty() && (value.is_empty() || value == "-") {
                    "0".to_string()
                } else {
                    value.to_string()
                };

                (integer, fraction)
            }
        };

        let decimal = match format!("{integer}{fraction}").parse::<i64>() {
            Ok(mantissa) => {
                let decimal = Decimal::new(mantissa, Self::precision());
                if decimal > Self::MAX {
                    Self::MAX
                } else if decimal < Self::MIN {
                    Self::MIN
                } else {
                    decimal
                }
            }
            Err(_) => Self::DEFAULT,
        };

        Self::from_decimal(decimal)
    }

    fn precision() -> u32 {
        Self::DEFAULT.scale()
    }

    fn to_string(&self) -> String {
        self.as_decimal().to_string()
    }

    fn can_increment(&self) -> bool {
        self.as_decimal() < &Self::MAX
    }

    fn can_decrement(&self) -> bool {
        self.as_decimal() > &Self::MIN
    }

    fn step(step_size: &StepSize) -> Decimal {
        match step_size {
            StepSize::Normal => Decimal::new(1, Self::precision()),
            StepSize::Large => Decimal::new(10, Self::precision()),
        }
    }

    fn increment(&self, step_size: &StepSize) -> Self {
        let new = self.as_decimal() + Self::step(step_size);

        Self::from_decimal(if new > Self::MAX { Self::MAX } else { new })
    }

    fn decrement(&self, step_size: &StepSize) -> Self {
        let new = self.as_decimal() - Self::step(step_size);

        Self::from_decimal(if new < Self::MIN { Self::MIN } else { new })
    }
}

pub enum StepSize {
    Normal,
    Large,
}

/// The local state of a [`DecimalInput2`].
#[derive(Debug, Clone)]
pub struct Content<Decimal>
where
    Decimal: NumberInput,
{
    inner: RefCell<Inner<Decimal>>,
}

#[derive(Debug, Clone)]
struct Inner<Decimal>
where
    Decimal: NumberInput,
{
    last_valid_value: Decimal,
    value: String,
}

impl<Decimal> Content<Decimal>
where
    Decimal: NumberInput,
{
    pub fn new(value: Decimal) -> Self {
        let scale = <Decimal as NumberInput>::precision();
        assert_eq!(<Decimal as NumberInput>::MIN.scale(), scale);
        assert_eq!(<Decimal as NumberInput>::MAX.scale(), scale);
        assert!(
            (<Decimal as NumberInput>::MIN..=<Decimal as NumberInput>::MAX)
                .contains(&<Decimal as NumberInput>::DEFAULT)
        );
        assert!(
            (<Decimal as NumberInput>::MIN..=<Decimal as NumberInput>::MAX)
                .contains(value.as_decimal())
        );

        let inner_value = value.to_string();
        Self {
            inner: RefCell::new(Inner {
                value: inner_value,
                last_valid_value: value,
            }),
        }
    }

    fn value(&self) -> String {
        let inner = self.inner.borrow();

        inner.value.clone()
    }

    fn with_inner_mut(&self, f: impl FnOnce(&mut Inner<Decimal>)) {
        let mut inner = self.inner.borrow_mut();

        f(&mut inner);
    }
}

/// The theme catalog of a [`DecimalInput2`].
pub trait Catalog: text_input::Catalog + button::Catalog {
    /// The default class for the text input of the [`DecimalInput2`].
    fn default_input<'a>() -> <Self as text_input::Catalog>::Class<'a> {
        <Self as text_input::Catalog>::default()
    }

    /// The default class for the menu of the [`DecimalInput2`].
    fn default_button<'a>() -> <Self as button::Catalog>::Class<'a> {
        <Self as button::Catalog>::default()
    }
}

impl Catalog for iced::Theme {}

impl<'a, T, Message, Theme, Renderer> From<DecimalInput2<'a, T, Message, Theme, Renderer>>
    for Element<'a, Message, Theme, Renderer>
where
    T: NumberInput + 'static,
    Message: Clone + 'a,
    Theme: Catalog + button::Catalog + widget::text::Catalog + 'a,
    Renderer: advanced_text::Renderer + 'a,
    <Renderer as advanced_text::Renderer>::Font: From<Font>,
{
    fn from(decimal_input: DecimalInput2<'a, T, Message, Theme, Renderer>) -> Self {
        Self::new(decimal_input)
    }
}

A couple of bits and pieces there are specific for my usecase (the codepoint for the up/down arrow icons, the lack of builder methods on the main struct, etc). There’s also one or two extraneous bits left in which need factoring out when I next look at it.

It uses rust_decimal::Decimal internally, and for a user to actually use the widget, they need to define a newtype which wraps Decimal and impls NumberInput (terrible name). The NumberInput trait requires the definition of a min, max and default value, as well as a way to produce the newtype from a Decimal and convert their newtype to a Decimal.

@airstrike I felt that given how much custom handling I need in terms of validating partial inputs, controlling focus of the text_input, etc, it would be better as a Widget, although I agree that most (all?) could be done as a ā€œComponentā€ pattern as per the unofficial guide(s).

Edit: just to add, I know that there’s a lot of scope to refactor pretty much all of this. In particular, I think if let chains would be really beneficial once it hits stable as the nesting gets pretty intense. Breaking out a lot of the logic into standalone functions would make handling control flow/returning early much clearer, too.

1 Like