The Viewable Pattern
The viewable pattern is an extension of the view-helper pattern, which allows for a bit cleaner code on the call site.
A viewable is a Struct which is build during the view function in your app and implements Into<iced::Element>
.
In practive, it behaves and is used like any other iced widget,
it may contain other iced::Element
s or references to your app state, like a &str
.
Dependencies
To create a viewable, we'll start with creating it's struct, which contains all dependencies we'll need to build our view tree later.
A viewable should avoid owning data and instead prefer references, if possible.
// 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()
}
}
In our case, we want to display an arbitrary Element
, which could be a text, an image or maybe even a row with both.
Additionally, we want enable adding a delete and an edit button. Since these buttons will always look the same, all we need is the Message which should be triggered when the button is clicked.
NOTE: If you are familiar enough with rust's type system, you could also use
To make those additional buttons optional, we'll use an Option<Message>
.
The Builder
A viewable usually uses the builder pattern to simplify creation and make the callsite more readable. The recommended builder pattern requests all mandatory dependencies in the constructor, while adding optional ones with chainable methods.
NOTE: If you are familiar enough with Rust's type system, you could also use a typestate builder to ensure all required dependencies are provided and to control the addition of optional elements.
In practice, this isn't always the best idea, because it creates more boilerplate and duplicate code.
// 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()
}
}
Creating The View
The last part of creating a Viewable is to build the actual view tree, as you would for your application.
To allow calling .into()
as we would with a normal widget, we'll implement the From trait for iced::Element
.
// 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()
}
}
If you want to support custom themes or additional renderers,
you'll have to specify the additional generic parameters for iced::Element
and set the constraints according to what you need.
Using The Viewable
to use the viewable, we can leverage the builder pattern we just implemented, followed by a call to .into()
.
In this case, we're only using on_delete
.
This will cause the viewable to add a delete button, but to forfeit the edit button.
impl App {
fn view(&self) -> iced::Element<Message> {
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()
}
}
Conclusion
The viewable pattern is great way to build your own "widgets", especially since you can make using them really ergonomic.
In some cases, a viewable might be overkill - you may be interested in the View-Helper for those times.
A Viewable also can't hold any application state. For that, you could take a look at the Component Pattern.