As discussed in #89, users expect a _blinking_ cursor to signify the current insert position, of a focused text input.
However, implementing this is more complicated than simply updating the TextInput widget, because currently iced widgets can only update in response to user events, or application messages (via the Subscription system).
This document aims to walk through this problem space and propose a minimal solution to enable a blinking TextInput.
TextInput widgetCurrently, the cursor is always shown (ie: it does not blink) for a focused text input. No cursor is shown for a TextInput which does not have focus.
All changes to the text-input's appearance are driven by user-interaction events.
Underlying iced widgets is an Event Loop, whose role is to bring together all the layers of iced. Think of this as the 'dispatch' or traffic light of an iced GUI - it controls the flow. The 'runtime' is composed of a number of separate components (Application, user_interface etc), but these details can be omitted for the sake of this explanation.
The runtime's job is to perform initial setup and then perform the following operations in sequence:
Widgets communicate integrate with the rest of iced (the 'runtime') by implementing the Widget trait. This trait defines:
width(), height()), and resolve its position (layout())draw())on_event())Notably absent from this trait is any means to indicate to the underlying runtime that an update needs to occur at some time in the future: This current design assumes all updates are the result of user interaction or application events.
Hopefully the background section explains enough that these points are clear:
iced event loop ('runtime') only updates/re-draws its widgets in response to a user or application event. It has no mechanism to redraw itself on its own whim.iced Widgets cannot communicate to the runtime that an update/re-draw needs to happen at some point in the future - no such interface exists.TextInput only updates in response to user interaction, and has no concept of time, let alone a sense of 'blink me 500ms after the last interaction'.Even though this RFC is written to support a blinking text-input, the more general case of a widget wanting to animate itself is a common use-case. We can generalize this problem to that of _periodic animation_ - providing the ability for widgets to update their appearance based on the passage of time. More details on different animation use-cases, and ideas around implementing this can be found in #31 / https://github.com/hecrj/iced/issues/31#issuecomment-703176958.
The event loop needs to be able perform an update cycle (ie: handle events if any, update layout if needed, and redraw) when a widget needs it. In the case of a blinking text input cursor, this would be every 500ms, to show/hide the input cursor.
For periodic updates such as these, the behavior of the _Wait_ state needs to be changed. Instead of waiting till the next application event or user interaction, it needs to wait till either the next time a widget needs to be updated, or on the next UI/application event, whichever is first.
For simplicity, we emit a new event AnimationTick when an animation tick occurs. This event will drive the update/draw cycle for us.
Widgets need to be able to communicate their need for a update/draw cycle at some time in the future to the event loop, as ultimately the event loop is responsible for updating that cycle.
This necessitates a change to the interface between widgets and the runtime: the Widget trait.
The exact change to enable this communication is a matter of API design. However, any change which communicates the soonest moment a widget would need to update would work.
TextInput widgetThe logic of the widget would need to be updated to:
iced runtime only performs an update/draw cycle in response to application events and user interaction, but to support periodic animation, we need the runtime to perform a cycle whenever a widget needs it._The numbering of these points aligns to that of the guided explanation in the previous section_.
The event loop needs to be changed to track the soonest moment which an update needs to occur, and to perform an update/draw cycle at this moment.
The intermediate state of a user interface (at the transition of a update/draw cycle) is stored in the State struct, from native/src/program/state.rs. A new field, next_animation_draw: AnimationState can be added to track the next draw required by widgets. More on the AnimationState type later, but just know that this type encapsulates the animation requirements of widgets, and this value can be obtained by calling a method on the root widget.
Modifying the behavior of the wait state is surprisingly trivial, thanks to the underlying use of winit. Instead of setting the event loops' control_flow to Wait, we just set it to WaitUntil( <time of soonest update> ). This changes the behavior to wait until the next event, or the provided time (whichever is sooner).
An update is needed to the Widget trait to communicate the animation requirements of a widget - in our case, that the TextInput widget needs to be re-drawn in 500ms.
We propose adding a new method to the trait, with a default implementation that just indicate no animation is taking place:
fn next_animation(&self) -> AnimationState {
AnimationState::NotAnimating
}
Widgets that need to animate (such as our TextInput) can implement this method to indicate an animation is required:
fn next_animation(&self) -> AnimationState {
AnimationState::AnimateIn(std::time::Instant::now().checked_add(Duration::from_millis(500)))
}
Remaining consistent with the stateless and intuitive feel of the widget trait, next_animation is called as part of the update loop, to get the latest set of animation requirements.
TextInput changesThe widget needs to keep track of the last time a user interacted with the input. We can do this by adding a new field to an internal state type Cursor:
pub struct Cursor {
state: State,
updated_at: Instant, // new field
}
And setting its value when the cursor state is updated:
pub(crate) fn move_to(&mut self, position: usize) {
self.updated_at = Instant::now(); // add this line
self.state = State::Index(position);
}
As a corner case, we also need to detect when the input is clicked and gains focus, which we can do by setting updated_at during on_event when we detect that condition.
AnimationState typeWidgets need to symbolize their animation requirements to the runtime. We propose creating a new enum type to represent this:
pub enum AnimationState {
NotAnimating, // The widget does not need to animate itself, and will only change in response to events.
AnimateIn(std::time::Instant), // The widget needs to animate itself no sooner than the provided moment.
}
A new type seems ideal for the following reasons:
AnimationState can implement std::cmp::Ord to provide the soonest animation time through min(). This will greatly simplify implementation because widgets which contain widgets need only return the min() of their contained widgets AnimationState values, in response to a next_animation call.Option<std::time::Instant>, but the intent is less obvious then an aptly-named enum.Source
use std::cmp::Ordering;
use std::time::Instant;
/// Animation requirements of a widget.
///
/// NotAnimating is greater than any value of AnimateIn. This allows the use of min() to reduce
/// a set of [`AnimationState`] values into a value representing the soonest needed animation.
///
/// [`AnimationState`]: struct.AnimationState.html
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnimationState {
/// The widget is not animating. It will only change in response to events or user interaction.
NotAnimating,
/// The widget needs to animate itself at the provided moment.
AnimateIn(Instant),
}
impl Ord for AnimationState {
fn cmp(&self, other: &Self) -> Ordering {
match (self, other) {
(AnimationState::NotAnimating, AnimationState::NotAnimating) => {
Ordering::Equal
}
(_, AnimationState::NotAnimating) => Ordering::Less,
(AnimationState::NotAnimating, _) => Ordering::Greater,
(AnimationState::AnimateIn(a), AnimationState::AnimateIn(b)) => {
a.cmp(b)
}
}
}
}
impl PartialOrd for AnimationState {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{Duration, Instant};
#[test]
fn ordering() {
let now = Instant::now();
let (less, more) = (
now.checked_add(Duration::from_millis(1)).unwrap(),
now.checked_add(Duration::from_millis(10)).unwrap(),
);
// Eq
assert_eq!(AnimationState::NotAnimating, AnimationState::NotAnimating);
assert_eq!(
AnimationState::AnimateIn(now),
AnimationState::AnimateIn(now)
);
// PartialOrd
assert!(AnimationState::AnimateIn(now) < AnimationState::NotAnimating);
assert!(AnimationState::NotAnimating > AnimationState::AnimateIn(now));
assert!(
AnimationState::AnimateIn(less) < AnimationState::AnimateIn(more)
);
assert!(
AnimationState::AnimateIn(more) > AnimationState::AnimateIn(less)
);
// Ord
assert!(AnimationState::AnimateIn(now) <= AnimationState::NotAnimating);
assert!(AnimationState::NotAnimating >= AnimationState::AnimateIn(now));
assert!(
AnimationState::AnimateIn(less) <= AnimationState::AnimateIn(more)
);
assert!(
AnimationState::AnimateIn(more) >= AnimationState::AnimateIn(more)
);
assert!(
AnimationState::AnimateIn(now) <= AnimationState::AnimateIn(now)
);
assert!(
AnimationState::AnimateIn(now) >= AnimationState::AnimateIn(now)
);
}
}
next_animation()) increases the runtime complexity of the update loop. Further, even though the method has a default implementation, its one more thing to think about when implementing a widget.iced, _all_ widgets will be updated & drawn whenever an animation is needed.on_event and messages to signal to the runtime that a redraw is neededMessage is an associated type, so it will either need to be wrapped somehow or converted into a Tuple. Also, the message-based approach seems harder to reason about than a method that signals current animation requirements.draw()draw() and makes it no longer free of side-effectsAnimationState and associated handling in the event loop.Please let me know what you think. Thanks! =D
Very nice! I have one question. Will this supposed solution make it harder to do incremental drawing, or is it something that can be added later? Some of the widgets I'm creating can get pretty complex, and I imagine text is really expensive too. We can create a separate RFC for this too if you want.
Incremental rendering / persistent widget tree is such a huge change that everything is likely to change anyway, but I think this is a step in the right direction.
Definitely want to develop incremental rendering in a separate RFC.
I have lots of ideas :joy: but I'll keep them out of this RFC.
I implemented this offline to see if there were any gotchas I missed. The only thing ive had to add to this RFC is the addition of a new synthetic event, AnimationTick, which is emitted when an animation fires.
Emitting a new event like this is the easiest way to drive the update cycle, and keeps the simple adage (widgets are only updated in response to messages + events) true. The alternative (making another entry point into the update cycle) seems suboptimal, as it increases complexity.
If you would like to test the prototype and give feedback, update your Cargo.toml with:
iced = { git = "https://github.com/twitchyliquid64/iced", branch = "text_input" }
iced_native = { git = "https://github.com/twitchyliquid64/iced", branch = "text_input" }
iced_graphics = { git = "https://github.com/twitchyliquid64/iced", branch = "text_input" }
# etc for all `iced` crates
You can see documentation for the updated trait here: https://iced-animations-rfc.tomdnetto.net/iced_native/widget/trait.Widget.html
Most helpful comment
Incremental rendering / persistent widget tree is such a huge change that everything is likely to change anyway, but I think this is a step in the right direction.
Definitely want to develop incremental rendering in a separate RFC.
I have lots of ideas :joy: but I'll keep them out of this RFC.