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