iced_pure 0.2.2

Pure widgets for Iced
Documentation

Stateless, pure widgets for iced.

The Elm Architecture, purity, and continuity

As you may know, applications made with iced use The Elm Architecture.

In a nutshell, this architecture defines the initial state of the application, a way to view it, and a way to update it after a user interaction. The update logic is called after a meaningful user interaction, which in turn updates the state of the application. Then, the view logic is executed to redisplay the application.

Since view logic is only run after an update, all of the mutations to the application state must only happen in the update logic. If the application state changes anywhere else, the view logic will not be rerun and, therefore, the previously generated view may stay outdated.

However, the Application trait in iced defines view as:

pub trait Application {
fn view(&mut self) -> Element<Self::Message>;
}

As a consequence, the application state can be mutated in view logic. The view logic in iced is impure.

This impurity is necessary because iced puts the burden of widget continuity on its users. In other words, it's up to you to provide iced with the internal state of each widget every time view is called.

If we take a look at the classic counter example:

struct Counter {
value: i32,
increment_button: button::State,
decrement_button: button::State,
}

// ...

impl Counter {
pub fn view(&mut self) -> Column<Message> {
Column::new()
.push(
Button::new(&mut self.increment_button, Text::new("+"))
.on_press(Message::IncrementPressed),
)
.push(Text::new(self.value.to_string()).size(50))
.push(
Button::new(&mut self.decrement_button, Text::new("-"))
.on_press(Message::DecrementPressed),
)
}
}

We can see how we need to keep track of the button::State of each Button in our Counter state and provide a mutable reference to the widgets in our view logic. The widgets produced by view are stateful.

While this approach forces users to keep track of widget state and causes impurity, I originally chose it because it allows iced to directly consume the widget tree produced by view. Since there is no internal state decoupled from view maintained by the runtime, iced does not need to compare (e.g. reconciliate) widget trees in order to ensure continuity.

Stateless widgets

As the library matures, the need for some kind of persistent widget data (see #553) between view calls becomes more apparent (e.g. incremental rendering, animations, accessibility, etc.).

If we are going to end up having persistent widget data anyways... There is no reason to have impure, stateful widgets anymore!

And so I started exploring and ended up creating a new subcrate called iced_pure, which introduces a completely stateless implementation for every widget in iced.

With the help of this crate, we can now write a pure counter example:

struct Counter {
value: i32,
}

// ...

impl Counter {
fn view(&self) -> Column<Message> {
Column::new()
.push(Button::new("Increment").on_press(Message::IncrementPressed))
.push(Text::new(self.value.to_string()).size(50))
.push(Button::new("Decrement").on_press(Message::DecrementPressed))
}
}

Notice how we no longer need to keep track of the button::State! The widgets in iced_pure do not take any mutable application state in view. They are stateless widgets. As a consequence, we do not need mutable access to self in view anymore. view becomes pure.