Full Code of the Example

main.rs:

mod list_item;
mod new_joke;

fn main() {
    iced::run("Project Structure Example", App::update, App::view).unwrap();
}

#[derive(Debug, Clone)]
enum Message {
    // This message is used to handle the new views message
    NewJoke(new_joke::Message),
    OpenNewJokeComponent,
    Delete(usize),
}

#[derive(Default)]
enum View {
    #[default]
    ListJokes,
    // This holds our new joke components state
    NewJoke(new_joke::NewJoke),
}

#[derive(Default)]
struct App {
    view: View,
    items: Vec<String>,
}

impl App {
    fn update(&mut self, message: Message) -> iced::Task<Message> {
        match message {
            Message::NewJoke(view_message) => {
                // as with all enums in rust, we'll need to use an if-let expression
                // to get access to our component from the `View` enum
                if let View::NewJoke(edit) = &mut self.view {
                    // Call the update method of the edit view
                    // and handle the returned action
                    match edit.update(view_message) {
                        // The none action is a no-op
                        new_joke::Action::None => {}

                        // If the action is a task, we'll need to map it, to ensure it returns the right Message type.
                        // This is the exact same as with `view` and the returned `iced::Element`
                        new_joke::Action::Run(task) => return task.map(Message::NewJoke),

                        // If the action is a cancel, switch back to the list view
                        new_joke::Action::Cancel => {
                            self.view = View::ListJokes;
                        }

                        // If the action is a submit, add the new joke before returning to the list view
                        new_joke::Action::Submit(new_joke_content) => {
                            self.view = View::ListJokes;
                            self.items.push(new_joke_content);
                        }
                    }
                }
            }
            Message::OpenNewJokeComponent => {
                // Create a new component
                let component = new_joke::NewJoke::new();
                self.view = View::NewJoke(component);
            }
            Message::Delete(index) => {
                self.items.remove(index);
            }
        }
        iced::Task::none()
    }

    fn view(&self) -> iced::Element<Message> {
        match &self.view {
            View::ListJokes => {
                let items = self
                    .items
                    .iter()
                    // since we want deletion, we'll need the index of each item, so we know which one to delete
                    .enumerate()
                    .map(|(index, item)| {
                        // create a listitem for each joke
                        list_item::ListItem::new(iced::widget::text(item))
                            // save the index to delete in the message
                            .on_delete(Message::Delete(index))
                            // since we implemented the `From` trait, we can just use into() to create an element,
                            // just as if we were using a widget
                            .into()
                    })
                    .collect();

                iced::widget::column![
                    iced::widget::button("New").on_press(Message::OpenNewJokeComponent),
                    iced::widget::Column::from_vec(items)
                ]
                // Some spacing goes a long way to make your UI more visually appealing
                .spacing(10)
                .into()
            }
            // If the view is an edit view, call the view method of the edit view
            // and map the returned message to the higher level message
            View::NewJoke(new_joke) => new_joke.view().map(Message::NewJoke),
        }
    }
}

list_item.rs:


// Depending on your use case, you can instead also
// accept types like `&str` or other references to your app state.
pub struct ListItem<'a, Message> {
    item: iced::Element<'a, Message>,
    on_delete: Option<Message>,
    on_edit: Option<Message>,
}

impl<'a, Message> ListItem<'a, Message> {
    // if you can, prefer using `impl Into` for other elements.
    // It makes the callsite look much nicer.
    pub fn new(item: impl Into<iced::Element<'a, Message>>) -> Self {
        Self {
            item: item.into(),
            on_delete: None,
            on_edit: None,
        }
    }

    pub fn on_delete(mut self, message: Message) -> Self {
        self.on_delete = Some(message);
        self
    }

    pub fn on_edit(mut self, message: Message) -> Self {
        self.on_edit = Some(message);
        self
    }
}

impl<'a, Message> From<ListItem<'a, Message>> for iced::Element<'a, Message>
where
    Message: Clone + 'a,
{
    // Here you can put the code which builds the actual view.
    fn from(item_row: ListItem<'a, Message>) -> Self {
        let mut row = iced::widget::row![item_row.item]
            // In your viewable, you can handle things like spacing and alignment,
            // just like you would in your view function.
            .spacing(10);

        if let Some(on_delete) = item_row.on_delete {
            row = row.push(iced::widget::button("Delete").on_press(on_delete));
        }

        if let Some(on_edit) = item_row.on_edit {
            row = row.push(iced::widget::button("Edit").on_press(on_edit));
        }

        row.into()
    }
}

new_joke.rs:


#[derive(Debug, Clone)]
pub enum Message {
    ChangeContent(String),
    RandomJoke,
    Submit,
    Cancel,
}

pub enum Action {
    // The user was happy with the joke and wants to submit it to the list
    Submit(String),
    // The user wants to cancel adding a new joke
    Cancel,
    // The components needs to run a task
    Run(iced::Task<Message>),
    // The component does not require any additional actions
    None,
}

pub struct NewJoke {
    joke: String,
}

impl NewJoke {
    pub fn new() -> Self {
        Self {
            joke: String::new(),
        }
    }
}

impl NewJoke {
    pub fn view(&self) -> iced::Element<Message> {
        iced::widget::column![
            iced::widget::text_input("Content", &self.joke)
                // on_input expects a closure, which would usually look like this:
                // |new_value| Message::ChangeContent(new_value)
                // Thankfully, you can just use the enum variants name directly
                .on_input(Message::ChangeContent),
            iced::widget::button("Random Joke").on_press(Message::RandomJoke),
            iced::widget::row![
                iced::widget::button("Cancel").on_press(Message::Cancel),
                iced::widget::button("Submit").on_press(Message::Submit)
            ]
            .spacing(10),
        ]
        .padding(10)
        .spacing(10)
        .into()
    }
}

impl NewJoke {
    #[must_use]
    pub fn update(&mut self, message: Message) -> Action {
        match message {
            Message::Submit => Action::Submit(self.joke.clone()),
            Message::Cancel => Action::Cancel,
            Message::ChangeContent(content) => {
                self.joke = content;
                Action::None
            }
            Message::RandomJoke => Action::Run(Self::random_joke_task()),
        }
    }

    fn random_joke_task() -> iced::Task<Message> {
        iced::Task::future(async {
            // Fetch a joke from the internet
            let client = reqwest::Client::new();
            let response: serde_json::Value = client
                .get("https://icanhazdadjoke.com")
                .header("Accept", "application/json")
                .send()
                .await
                .unwrap()
                .json()
                .await
                .unwrap();

            // Parse the response
            let joke = response["joke"].as_str().unwrap();

            // Return the joke as a message
            Message::ChangeContent(joke.to_owned())
        })
    }
}