Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stateless widgets #1284

Merged
merged 57 commits into from
Mar 23, 2022
Merged

Stateless widgets #1284

merged 57 commits into from
Mar 23, 2022

Conversation

hecrj
Copy link
Member

@hecrj hecrj commented Mar 14, 2022

This PR introduces a pure, stateless version of every widget in iced alongside a new Pure widget to embed them in an impure, stateful application.

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. reconcile) 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.

After the changes in this PR, 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.

All of the widgets in iced_native, iced_graphics, and iced_lazy have a pure counterpart! In order to use any of them, you will need to enable the new pure feature. For instance:

iced = { git = "https://github.com/iced-rs/iced.git", features = ["pure"] }

Once the pure feature is enabled, you can find the new widgets in the pure module.

use iced::pure::widget::Button;

let hello_button = Button::new("Hello!");

For the optional widgets (e.g. Canvas, Image, etc.) you will still need to enable the specific widget feature, alongside the pure feature.

Additionally, I have introduced function helpers to allow you to use widgets with less verbosity:

use iced::pure::button;

let hello_button = button("Hello!");

Let me know if you have any ideas to further reduce the boilerplate in view code.

Purifying applications

The pure module also offers pure versions of the Application and Sandbox traits, so you can get started with pure iced applications right away.

The following examples have a new pure version and showcase how to use the new pure module:

However, if you already have an existing iced application, you do not need to switch completely to the new traits in order to benefit from the pure module. Instead, you can leverage the new Pure widget to include pure widgets in your impure Application.

For instance, let's say we want to use our pure Counter in an impure application:

use iced::pure::{self, Pure};
use iced::{Sandbox, Element};

struct Impure {
    state: pure::State,
    counter: Counter,
}

impl Sandbox for Impure {
    // ...

    pub fn view(&mut self) -> Element<Self::Message> {
        Pure::new(&mut self.state, self.counter.view()).into()
    }
}

Pure acts as a bridge between pure and impure widgets. It is completely opt-in and can be used to slowly migrate your application to the new architecture.

The purification of your application may trigger a bunch of important refactors, since it's far easier to keep your data decoupled from the GUI state with stateless widgets. For this reason, I recommend starting small in the most nested views of your application and slowly expand the purity upwards.

In the long run, the plan is to make pure the default API and deprecate the impure widgets altogether.

Other changes

This PR also contains a bunch of smaller changes worth mentioning:

  • The glow_canvas and glow_qr_code features have been removed. Now you can simply enable canvas or qr_code together with glow.
  • The generic Message in the canvas::Program trait has been turned into an associated type.
  • iced_wgpu, iced_glow, and iced_graphics no longer re-export aliases of all the widgets. This caused a lot of unnecessary duplication. If you need widgets that are renderer-agnostic, you can depend on iced_native directly.
  • A bunch of widget logic in iced_native (layout, update, draw, etc.) has been isolated into functions in its respective widget module.
  • &'static str implements Into<Element> by turning it into pure::Text. This is very useful to avoid typing Text::new everywhere!

And I think that's all! This is maybe the biggest user-facing change since the inception of the library. Give it a shot and let me know what you think!

The idea here is to expose a set of "virtual widgets" that can be used with a
`Virtual` widget and its `virtual::State`.

A virtual widget is a widget that does not contain any state, but instead is a
"virtual" representation of the "real" widget. The real widgets are stored in
the `virtual::State`.

Every time a new virtual widget tree is created during `view`, it is compared to
the previous one and "real" widgets are added / removed to the `virtual::State`.

Effectively, this removes the need to keep track of local widget state in the
application state and allows `view` to take an immutable reference to `self`.

To summarize, using this crate should allow users to remove `State` structs
in their application state.

Eventually, the strategy used here may be adopted generally and, as a result,
all of the widgets in `iced_native` would be replaced!
... as well as a very naive diffing strategy!
`virtual` is a reserved keyword in Rust 😬
Besides exposing the `iced_pure` crate, enabling the `pure` feature also
provides pure versions of both the `Application` and `Sandbox` traits!
:tada:
`button("Hello")` is easier to write and read than
`Button::new("Hello")`.
... and reuse it in `iced_pure`!
The `Widget` trait in `iced_pure` needed to change a bit to make the
implementation of `Element::map` possible.

Specifically, the `children` method has been split into `diff` and
`children_state`.
@hecrj hecrj added this to the 0.4.0 milestone Mar 14, 2022
... and fix collisions with the new `helpers`
As it is useful to make the `Message` completely free in many
implementations.
This helper should be unnecessary in the future.
@alex13sh
Copy link

I'm wondering what the scrollable example would look like, where the scrollable::State::snap_to function is used in Application::update

@hecrj
Copy link
Member Author

hecrj commented Mar 21, 2022

@alex13sh Querying / modifying internal widget state is not possible in this iteration.

But there are 2 main ideas to satisfy this use case:

  • Exposing a Command API in every widget module.
  • Allowing users to explicitly track internal state when necessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants