How to make an advanced button widget?

I want to make a widget based of a button and responsive.
I’m after:

A main text, centered in the button.
An optional sub text in a corner of the button.
Default is to fill the space.
Support for on_hover and end_hover.
on_press and on_release

Whats the best way to go about doing this?
PS. Ive been using iced 2 days, so you might need to explain a bit more than usual!

Writing a Widget is not hard but it has a steep learning curve as there are several concepts you will need to juggle.

Before going down that path, may I ask why not compose this functionality using existing widgets such as mouse_area and stack?

If the issue is reusability, you could create a function that is generic over Message and looks somewhat like this:

fn my_widget<'a, Message>(
    content: &'a str,
    footnote: Option<&'a str>,
    on_enter: Message,
    on_exit: Message,
    on_press: Message,
    on_release: Message,
) -> Element<'a, Message>
where
    Message: Clone + 'static,
{
    let footnote = container(match footnote {
        Some(footnote) => text(footnote).size(12).into(),
        None => Element::from(horizontal_space()),
    })
    .align_bottom(Fill)
    .align_right(Fill)
    .width(Fill)
    .height(Fill)
    .padding(2);

    container(
        mouse_area(stack![
            center(text(content).size(16)).width(Fill).height(Fill),
            footnote
        ])
        .on_enter(on_enter)
        .on_exit(on_exit)
        .on_press(on_press)
        .on_release(on_release),
    )
    .style(container::rounded_box)
    .into()
}

A full example is below:

use iced::widget::{center, column, container, horizontal_space, mouse_area, stack, text};
use iced::{Element, Fill, Size, Task};

fn main() -> iced::Result {
    iced::application("iced • mouse area example", App::update, App::view)
        .window_size(Size::new(400.0, 200.0))
        .centered()
        .run()
}

#[derive(Debug, Clone)]
enum Message {
    MouseEnter,
    MouseExit,
    MousePress,
    MouseRelease,
}

#[derive(Default)]
struct App {
    state: &'static str,
}

impl App {
    fn update(&mut self, message: Message) -> Task<Message> {
        match message {
            Message::MouseEnter => self.state = "Mouse entered!",
            Message::MouseExit => self.state = "Mouse left!",
            Message::MousePress => self.state = "Mouse pressed!",
            Message::MouseRelease => self.state = "Mouse released!",
        }
        Task::none()
    }

    fn view(&self) -> Element<Message> {
        center(
            column![
                my_widget(
                    "Hover and click me!",
                    Some("(I'm the footnote)"),
                    Message::MouseEnter,
                    Message::MouseExit,
                    Message::MousePress,
                    Message::MouseRelease,
                ),
                text(self.state).size(14),
            ]
            .padding(20),
        )
        .padding(20)
        .into()
    }
}

fn my_widget<'a, Message>(
    content: &'a str,
    footnote: Option<&'a str>,
    on_enter: Message,
    on_exit: Message,
    on_press: Message,
    on_release: Message,
) -> Element<'a, Message>
where
    Message: Clone + 'static,
{
    let footnote = container(match footnote {
        Some(footnote) => text(footnote).size(12).into(),
        None => Element::from(horizontal_space()),
    })
    .align_bottom(Fill)
    .align_right(Fill)
    .width(Fill)
    .height(Fill)
    .padding(2);

    container(
        mouse_area(stack![
            center(text(content).size(16)).width(Fill).height(Fill),
            footnote
        ])
        .on_enter(on_enter)
        .on_exit(on_exit)
        .on_press(on_press)
        .on_release(on_release),
    )
    .style(container::rounded_box)
    .into()
}

If you still want to go down the widget path, I can give you some pointers.

Ok thanks, that’s very helpful!

I’ll have a play with that and see how I go. It should be fine for my needs.

At what point would I need I consider a full widget vs doing it this way?

I’m not sure there’s a short answer, although there’s probably a more technically accurate answer than mine, but I’d say a full widget would give you more freedom, so I think you’d do it whenever this feels very limiting. You might have a very specific behavior or UI feedback that you’d like to accomplish that may not be straightforward or possible at all to achieve with existing widgets.

For example, you’d have the freedom to layout any children nodes however you see fit—though for your current use case the snippet I shared seems to suffice.

I suppose more importantly you could handle any iced::Event directly instead of needing to rely on what I call the “bindings” of such events to Messages as provided by the existing widgets. Say, if you wanted to react to a keyboard event, which isn’t exposed by any of the .on_press, .on_release, etc. in the above snippet.

Or you might want to draw the widget differently depending on the cursor’s position, so that you could style the background differently while hovered.

Your widget could also perform Operations, but those are even more advanced than just writing a normal widget. The simplest operation is probably focusable, which lets widgets react to focus events so that they can focus/unfocus and generally be styled accordingly.

There’s also the possibility that you’re trying to model an inherently complex behavior, like a spreadsheet editor, or a board with connecting nodes, or a multi-line text input… Things that aren’t quite composable with the right degree of complex UI that would be expected out of those widgets, so whipping out a full impl Widget is the right path to go.

Hope that helps!

great! thanks, very insightful.

1 Like