Allow widgets to request a redraw at a specific time.
This is a necessary feature to render loading spinners, a blinking text cursor, GIF images, etc.
[winit] allows controlling the event loop in a flexible way. We may be able to use ControlFlow::WaitUntil for this purpose.
I'm not sure why a redraw at a specific time is necessary. Is this so that a component can render at e.g. a specific framerate? It seems like a timer or a separate scheduler to call update functions might be a way to go about this, followed by requesting a window redraw?
I need this too. I'm writing a program for the web that does custom animation. I looked at the solar_system and stopwatch example but they both use async_std to drive the animation with a timer. The async_std crate does not support WASM yet. See https://github.com/async-rs/async-std/issues/220
For a "normal" web program I would just use the Window.requestAnimationFrame method.
Hey @hecrj! I鈥檓 a noob but would love to take a shot at helping out with this. Are there any blockers for this issue?; and if not, which place(s) in the codebase would the change be made?
I have an idea for a simple but versatile Animation widget where different functional methods could be used to modify a child widget.
Perhaps something like this:
fn update(&mut self, message: Message){
match message {
Message::TriggerAnimation => self.animation_state.forward(), // If the animation has not been run yet, run it forward
}
}
fn view(&mut self) {
Button::new(&mut self.increment_button, Text::new("Trigger")).on_press(Message::TriggerAnimation)
Animation::new(&mut self.animation_state, child: Text::new("I just faded in!"))
.fade_in(Curve::linear()) // Fade in linearly
.translate(Curve::ease(), (-10, 0), (0,0)) // Move in from relative left
}
Following on from a conversation on the zulip chat, @hecrj mentioned a good start would be enumerating use-cases, and think through how animations/draw state will interact with other features and API design decisions (of particular note, async rendering, and layout/widget state).
In the interest of getting started, he's my attempt at rounding up the use-cases:
Self-animating widgets - We want to support widgets which need to change their appearance _as a consequence of the nominal passage of time_. Examples of this are spinners (loading animations), blinking text cursors, and animated GIFs.
We can probably divide this into two categories: those that want to re-draw as fast as possible (ie: every frame, to give the appearance of smooth motion), or those that want to re-draw at roughly some point in the future(ie: a blinking cursor, roughly every 500ms).
For the latter case, its common for the animation requirement to be temporary. For example, the cursor only needs to blink for a text input that is currently in focus.
Transitionary animations - We want to support widgets which temporarily animate a change in state. An example of this could be an image which fades in once the bitmap data is loaded, or a text field which highlights itself (and then fades back to normal) when its content changes.
Non-case: Updating a widget based on external/non-UI state. I believe iced_native::subscription::Recipe should be used for this purpose (but feel free to disagree with me).
Incremental layout/draw - The current implementation caches the state of the _layout_ (see native/src/user_interface.rs) through a Cache object, however has no such equivalent for caching drawable primitives for each widget (instead, draw() is called recursively throughout the entire widget tree whenever drawing is performed). This is nice and simple, but for a complex graph which elements which are animating every frame, this would result in a lot of draw() calls to widgets which don't change, which would be wasteful.
I imagine a future implementation might try and keep track of the draw/message/damage state of widgets, and avoid performing unnecessary computation here if theres no need. Alternatively, damage information for the entire visible surface could be tracked, and combined with a draw() walk could be used to cull draw() invocations across the tree. #377 is exploring the beginnings of damage tracking.
Widget methods - The widget tree is composed of types which implement layout(), on_event(), and draw(), which is an awesomely simple & understandable API. However, this doesn't really leave a good place for animation-related state.
markNeedsPaint().request_paint() to request a redraw when one is required.request_anim_frame() to mark itself and all parents as needing an animation specific redraw.request_timer() to setup a timer event which fires periodically.Note: not every "animation" is purely visual, e.g. in Qt menus, there is a short delay (maybe 500ms) between hovering over a menu item and opening/closing submenus. The reason is more functional than visual: during mouse movement the cursor may temporarily leave the menu item before entering the sub-menu's area, yet the user expects the sub-menu not to close during this brief period. (Alternative approaches are possible here, e.g. GTK does not close sub-menus when the cursor leaves the menu area, but does immediately do so if moving over a different item in the parent menu.)
KAS has a very simple solution similar to Flutter: let widgets request an update after dur: Duration, and make the widget handle redrawing (and optionally requesting another update-after-duration) in the event handler. (May have limitations but so far seems to work well.)
Most helpful comment
Following on from a conversation on the zulip chat, @hecrj mentioned a good start would be enumerating use-cases, and think through how animations/draw state will interact with other features and API design decisions (of particular note, async rendering, and layout/widget state).
In the interest of getting started, he's my attempt at rounding up the use-cases:
Self-animating widgets - We want to support widgets which need to change their appearance _as a consequence of the nominal passage of time_. Examples of this are spinners (loading animations), blinking text cursors, and animated GIFs.
We can probably divide this into two categories: those that want to re-draw as fast as possible (ie: every frame, to give the appearance of smooth motion), or those that want to re-draw at roughly some point in the future(ie: a blinking cursor, roughly every 500ms).
For the latter case, its common for the animation requirement to be temporary. For example, the cursor only needs to blink for a text input that is currently in focus.
Transitionary animations - We want to support widgets which temporarily animate a change in state. An example of this could be an image which fades in once the bitmap data is loaded, or a text field which highlights itself (and then fades back to normal) when its content changes.
Non-case: Updating a widget based on external/non-UI state. I believe
iced_native::subscription::Recipeshould be used for this purpose (but feel free to disagree with me).Thoughts:
Incremental layout/draw - The current implementation caches the state of the _layout_ (see
native/src/user_interface.rs) through aCacheobject, however has no such equivalent for caching drawable primitives for each widget (instead,draw()is called recursively throughout the entire widget tree whenever drawing is performed). This is nice and simple, but for a complex graph which elements which are animating every frame, this would result in a lot ofdraw()calls to widgets which don't change, which would be wasteful.I imagine a future implementation might try and keep track of the draw/message/damage state of widgets, and avoid performing unnecessary computation here if theres no need. Alternatively, damage information for the entire visible surface could be tracked, and combined with a
draw()walk could be used to culldraw()invocations across the tree. #377 is exploring the beginnings of damage tracking.Widget methods - The widget tree is composed of types which implement
layout(),on_event(), anddraw(), which is an awesomely simple & understandable API. However, this doesn't really leave a good place for animation-related state.Prior art / Implementation ideas
markNeedsPaint().request_paint()to request a redraw when one is required.request_anim_frame()to mark itself and all parents as needing an animation specific redraw.request_timer()to setup a timer event which fires periodically.