This proposal introduces the idea of widget operations in iced. A widget operation is defined as some logic that can traverse (and operate on) the widget tree of an iced application in order to query or update some widget state.
The non-pure widget API in iced forces users to explicitly store widget state in their application state. This approach, while cumbersome, has a nice benefit. Given that all of the widget state is explicitly defined in the application state, it's possible for users to easily modify the internal state of a specific widget during an update
.
For instance, if we have a TextInput
and a Button
in our application:
struct Example {
input: text_input::State,
button: button::State,
}
We can easily change the state of the input
in update
. For example, let's say we want to focus the input
after the button
is pressed. We can just write:
fn update(&mut self, message: Message) {
match message {
Message::ButtonPressed => {
self.input = text_input::State::focused();
}
// ...
}
}
Since we cannot expect users to manage and synchronize all of the widget internal state through The Elm Architecture, this is a very common and perfectly valid use case.
In #1284, a new widget API was introduced with the goal to replace the current non-pure approach. And precisely, the main difference is the lack of explicit widget state management.
In practice, this means that users do not have to store widget state in their application state anymore. But, as a consequence, users cannot change the internal state of a widget during an update
, since there is no widget state to refer to.
For instance, when using the pure widget API, the previous Example
struct would not hold any widget state:
struct Example;
Thus, it would not be possible to refer to the state of the TextInput
:
fn update(&mut self, message: Message) {
match message {
Message::ButtonPressed => {
// How do we focus the `TextInput` here?
}
// ...
}
}
If we want the new pure API to replace the current one, we need to introduce new ideas in iced that can be used to query and update internal widget state.
There are only two new ideas that end-users need to get familiarized with: operations and identifiers.
Widget modules can export widget operations in their public interface. A widget operation is a Command
and, as a result, it can be run by any implementor of the Application
trait.
For instance, we could satisfy the previous use case if the text_input
module exposed a focus
operation:
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::ButtonPressed => {
// We build the `focus` operation and return the resulting `Command`
text_input::focus(MY_TEXT_INPUT_ID)
}
// ...
}
}
In the example above, focus
is a function exported by the text_input
module that can be used to build a widget operation that focuses a specific TextInput
. Its output is a Command
that any Application
can run.
Since we may produce many different text inputs in our Application::view
, we need some way to identify the specific text input. As a consequence, some widget operations may need identifiers.
A widget identifier is an instance of some opaque type Id
exported by a widget module. A widget will only support identifiers if it exposes an operation that needs them.
For instance, the MY_TEXT_INPUT_ID
static in the previous example could be defined as follows:
lazy_static! {
static MY_TEXT_INPUT_ID: text_input::Id = text_input::Id::unique();
}
For any built-in widget, the unique
constructor of an Id
type always produces a different instance every time it's called:
use some_widget::Id;
let some_id = Id::unique();
let another_id = Id::unique();
assert_ne!(some_id, another_id);
Any built-in Id
type also generally offers a simple new
constructor that takes a String
, which can be used in view
code to generate identifiers from an external source of data at runtime (as opposed to using unique
at compile-time).
A widget that supports identifiers has an id
method that can be used to set the Id
in view
code, effectively connecting view
logic with any operation produced in update
.
For instance, if we include the following TextInput
in our view
:
text_input("Some placeholder", some_value, Message::InputChanged)
.id(MY_TEXT_INPUT_ID)
Then, returning the Command
produced by text_input::focus(MY_TEXT_INPUT_ID)
in Application::update
will cause this specific TextInput
to gain focus.
A new widget
method will be introduced to Command
in iced_native
:
impl<T> Command<T> {
pub fn widget(operation: impl widget::Operation<T>) -> Self {
Self::single(Action::Widget(Box::new(operation)))
}
}
As shown above, the implementation of Command::widget
will use a new command::Action
:
pub enum Action<T> {
// ...
/// Run a widget operation.
Widget(Box<dyn widget::Operation<T>>)
}
The new Command::widget
method will allow widget developers to expose a widget operation as a Command
. For instance, we could write text_input::focus
as follows:
use iced_native::command::{self, Command};
pub fn focus<Message>(id: Id) -> Command<Message> {
Command::widget(Focus(id))
}
As shown above, Command::widget
takes an implementor of a brand new trait: widget::Operation
.
The Operation
trait in the widget
module defines the logic of a widget operation. A widget operation usually represents some state that is changed as it traverses a widget tree, producing some output T
as a result.
An operation is split into different methods that represent different widget types. Widgets will call the applicable methods of the Operation
trait given their particular nature. An operation may be called multiple times per widget, if applicable.
In a way, the Operation
trait is analogous to the Hasher
trait, but the hashed values (the operands!) are the widgets themselves.
The trait is defined as follows:
use crate::widget::Id;
use crate::widget::state;
pub trait Operation<T> {
fn clickable(&mut self, state: &mut dyn operation::Clickable, id: Option<Id>);
fn focusable(&mut self, state: &mut dyn operation::Focusable, id: Option<Id>);
fn editable(&mut self, state: &mut dyn operation::Editable, id: Option<Id>);
fn text(&mut self, contents: &str, id: Option<Id>);
fn container(
&mut self,
_id: Option<Id>,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
);
// ...
// Further methods can be added for additional widget types!
// ...
fn finish(&self) -> Outcome<T>;
}
As shown, an operation can operate on different types of widget. Where applicable, the methods of the trait receive mutable widget state that can be used to query it or update it.
For instance, we could implement the focus
operation by leveraging the focusable
method:
use iced_native::command::{self, Command};
use iced_native::widget;
use iced_native::widget::state;
pub fn focus<Message>(id: Id) -> Command<Message> {
Command::widget(Focus(id))
}
struct Focus(Id);
impl<T> widget::Operation<T> for Focus {
fn focusable(&mut self, state: &mut dyn operation::Focusable, id: Option<Id>) {
// If the current widget has an identifier...
if let Some(candidate_id) = id {
// And it matches the identifier we are looking for...
if self.0 == id {
// Then, we focus the input!
state.focus();
}
}
}
fn container(
&mut self,
_id: Option<Id>,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
) {
// If the current widget is a container, we just keep traversing the tree.
operate_on_children(self)
}
fn finish(&self) -> Outcome<T> {
Outcome::None
}
// ...
}
In the focusable
method we look for any focusable widget that has the identifier we are interested in, then we use the generic Focusable
widget state to focus
that specific widget.
The container
implementation will be called by any widget that contains other widgets and, as a result, it can be leveraged to control the traversal of the widget tree. In this case, we just keep traversing the widget tree unconditionally.
An Operation
may produce some Outcome
when finish
is called. The focus
operation does not produce any.
The implementation of the other methods in this particular example is empty and has been omitted. In fact, the Operation
trait provides a default implementation for all of its methods except container
:
pub trait Operation<T> {
fn container(
&mut self,
_id: Option<Id>,
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
);
fn clickable(&mut self, state: &mut dyn operation::Clickable, id: Option<Id>) {}
fn focusable(&mut self, state: &mut dyn operation::Focusable, id: Option<Id>) {}
fn editable(&mut self, state: &mut dyn operation::Editable, id: Option<Id>) {}
fn text(&mut self, contents: &str, id: Option<Id>) {}
// ...
fn finish(&self) -> Outcome<T> {
Outcome::None
}
}
Therefore, an empty implementation of an Operation
will be the "identity" operation and will simply traverse the whole widget tree without doing anything.
For reusability, widget operations should be decoupled from particular widget implementations. Furthermore, it is necessary for operations to work even when custom widgets are present in a view
.
Because of this, the Operation
trait relies on a new set of generic traits meant to represent different kinds of widget state. For instance, instead of directly relying on text_input::State
, the Operation
trait takes an implementor of the operation::Focusable
trait:
pub trait Focusable {
fn is_focused(&self) -> bool;
fn focus(&mut self);
fn unfocus(&mut self);
}
This way, an Operation
can support virtually any widget, as long as the internal widget state implements the proper state traits. For example, text_input::State
will implement operation::Focusable
:
impl operation::Focusable for State {
fn is_focused(&self) -> bool {
State::is_focused(self)
}
fn focus(&mut self) {
State::focus(self)
}
fn unfocus(&mut self) {
State::unfocus(self)
}
}
Further widgets could choose to implement this trait for their internal state, depending on the nature of the widget. It is up to the implementation of a Widget
to properly support the Operation
API.
The Outcome
of an operation can take one of 3 shapes:
None
means the operation produced no result.Some
means the operation produced a result of typeT
. This typeT
will be, at the same time, the output of its respectiveCommand
. Generally, this will lead to a newMessage
being produced and fed toupdate
.Chain
means the operation produced a brand new operation as a result, and should be executed.
pub enum Outcome<T> {
None,
Some(T),
Chain(Box<dyn Operation<T>>),
}
In many cases, traversing the widget tree once may not be enough to complete an operation. For instance, an operation like text_input::focus_previous
needs to traverse the tree once to find the current focused widget, unfocus it, and then traverse it again to focus the previous focusable widget with the information gathered in the first traversal.
For this reason, it is possible for an operation to be split into many, chained smaller ones.
The shell implementations (i.e. iced_winit
and iced_glutin
, currently) will need to handle the new Widget
variant of command::Action
:
for action in command.actions() {
match action {
// ...
command::Action::Widget(operation) => {
let mut current_cache = std::mem::take(cache);
let mut current_operation = Some(action.into_operation());
let mut user_interface = build_user_interface(
application,
current_cache,
renderer,
state.logical_size(),
debug,
);
while let Some(mut operation) = current_operation.take() {
user_interface.operate(renderer, operation.as_mut());
match operation.finish() {
operation::Outcome::None => {}
operation::Outcome::Some(message) => {
proxy
.send_event(message)
.expect("Send message to event loop");
}
operation::Outcome::Chain(next) => {
current_operation = Some(next);
}
}
}
current_cache = user_interface.into_cache();
*cache = current_cache;
}
}
}
As shown above, UserInterface
will have a new operate
method:
impl<'a, Message, Renderer> UserInterface<'a, Message, Renderer>
where
Renderer: crate::Renderer,
Renderer::Theme: application::StyleSheet,
{
// ...
/// Applies a [`widget::Operation`] to the [`UserInterface`].
pub fn operate(
&mut self,
renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>,
) {
self.root.operate(Layout::new(&self.base), operation);
if let Some(layout) = self.overlay.as_ref() {
if let Some(overlay) =
self.root.overlay(Layout::new(&self.base), renderer)
{
overlay.operate(Layout::new(layout), operation);
}
}
}
}
This new method in turn uses a new operate
method on both the Widget
and Overlay
traits:
/// Applies an [`Operation`] to the [`Widget`].
fn operate(
&mut self,
_state: &mut Tree, // Only the pure traits have this argument
_layout: Layout<'_>,
_operation: &mut dyn Operation<Message>,
);
Each particular widget implementation will call the proper methods of the Operation
based on the nature of the widget.
For instance, TextInput
could implement operate
as follows:
impl<'a, Message, Renderer> Widget<Message, Renderer>
for TextInput<'a, Message, Renderer>
where
Message: Clone,
Renderer: text::Renderer,
Renderer::Theme: StyleSheet,
{
// ...
fn operate(
&self,
tree: &mut Tree,
_layout: Layout<'_>,
operation: &mut dyn widget::Operation<Message>,
) {
let state = tree.state.downcast_mut::<text_input::State>();
// We can call `focusable` since `text_input::State` implements
// `operation::Focusable`
operation.focusable(state, self.id);
// `editable` can be called as well, analogously.
operation.editable(state, self.id);
}
}
Other widgets will be implemented in a similar way!
There aren't many drawbacks to this proposal, since most of the changes described here do not break any existing, end-user-facing APIs. Instead, most of the changes revolve around new ideas without affecting the existing codebase.
The design described here is relatively simple. Since operations are exposed through the Command
API, end users only really need to get familiarized with the idea of identifiers.
Identifiers are common in the Web and other GUI toolkits, and so the friction for end users should be minor.
Furthermore, Elm uses a very similar design where internal widget state can be updated through tasks and identifiers as well.
The design should be generic enough to work well with custom widgets and, at the same time, should be extensible and allow us to iterate easily if new widgets types are necessary.
Finally, the implementation should be quite straightforward with no apparent hacks or important refactors necessary.
- Should we rename
Outcome
to something else? An operation producing aResult
makes sense, but aResult
type in Rust generally has an error variant and an operation cannot really fail as of now. - What other methods should we include in
widget::Operation
? Since many methods of an operation can be called per widget, should we include methods to mark the "start" and "end" of a widget?
- We could allow some methods of the generic state traits to produce messages as output. This would allow us to implement interesting use cases in the future (e.g. a test framework that queries and interacts with the GUI exclusively through operations).
- Many useful operations should be possible with this design and could be built-in in the library, like querying the current size of a particular widget or controlling a
Scrollable
programmatically.