An initial design for widget state, environment, and animation
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 9.7k
- Forks
- 565
- PR merge metrics
- No merged PRs in 30d
Description
# Environment and widget state
This is an attempt to sketch out a mechanism for controlling widget display properties (like colors or text size) in a way that allows support for theming, accessibility, and resumable/interruptable animations.
## Faux dynamic typing with enums
At the core of this proposal is the idea of using enums to store homogenous, per-widget data.
This involves using a custom enum that has members for each concrete type that is used while drawing:
```rust
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Value {
Point(Point),
Size(Size),
Rect(Rect),
Color(Color),
Float(f64),
}
```
Each of these concrete types implements `Into`, and `TryFrom` (we may wish to use a custom trait in place of `TryFrom`, but it works for prototyping):
```rust
let point = Point { x: 101.0, y: 42. };
let value: Value = point.into();
assert_eq!(Value::Point(Point { x: 101.0, y: 42. }), point);
let from_value: Point = value.try_into().expect("value of wrong concrete type");
```
To use these values, we put them in a map, and access them with a special getter method:
```rust
pub struct Variables {
store: HashMap,
}
impl Variables {
pub fn get>(&self, key: &str) -> Result {
match self.store.get(key)
.ok_or_else(format!("No entry for {}", key))
.and_then(|v| v.try_into())
}
}
```
This is the 'safe' version. In practice, this is probably unnecessarily cautious, and we can have `get` panic if the value is missing or of the incorrect type. The reason for this is that keys and their types should not change at runtime.
## How we can use this
### For Custom state
When a widget is instantiated, it has an opportunity to register custom keys and values that it will use when drawing:
```rust
trait Widget {
fn init_state(&self) -> Option {}
}
struct BouncingBall;
impl Widget for BouncingBall {
fn init_state(&self) -> Option {
let mut vars = Variables::new();
vars.set("ball.ypos", 0.0);
Some(vars)
}
fn paint(&mut self, paint_ctx: &mut PaintCtx, geom: &Geometry) {
let x = geom.min_x();
let y = geom.min_y() + paint_ctx.state.get("ball.ypos");
let circ = Circle::new((x, y), 40.);
let fill_color = paint_ctx.render_ctx.sold_brush("0xFF_AA_DD_FF");
paint_ctx.fill(circ, &fill_color, FillRule::NonZero);
}
}
```
The main motivation for this pattern is to allow drawing code to be animation aware.
### With animations:
In the [animate](https://github.com/xi-editor/druid/blob/animate/examples/anim.rs#L65) branch I've been experimenting with a more automated animation API. With this API animations are represented as a start and an end state, an animation _curve_, and a duration. The framework handles interpolating between the provided values; on each animation tick, the widget gets a callback with the current state for those values.
```rust
// on key down we create a new animation that includes a color and a float
fn key(&mut self, _event: &KeyEvent, ctx: &mut HandlerCtx) -> bool {
let anim = Animation::with_duration(2.0)
.adding_component("a_float", AnimationCurve::OutElastic, 1.0, 350.0)
.adding_component(
"a_color",
AnimationCurve::Linear,
0xFF_00_00_FF,
0x00_00_FF_FF,
);
ctx.animate(anim);
true
}
// on each animation tick, we receive the active animation, and can access the interpolated values
// this works just like the `Variables` dictionary above.
fn animate(&mut self, anim: &Animation, ctx: &mut HandlerCtx) {
self.0 = anim.current_value("a_float");
self.1 = anim.current_value("a_color");
ctx.request_anim_frame();
}
```
This works okay, but introduces an annoying step: when you get the callback, you have to stash the updated values somewhere, request a frame, and use them when drawing.
### Combining with custom widget state:
If, however, we combine this with the custom widget state discussed above, we can make everything _just work_; the keys in the animation can refer to keys declared as part of the widget's state, and then while animating we can just provide the correct interpolated values in the state that gets passed to the draw call.
In this world, then, if we want to animate a color change of the window background on a key press, we can just do:
```rust
impl Widget for BouncingBall {
fn init_state(&self) -> Option {
Some(variables!["bg_color", Color::Red])
}
fn paint(&mut self, paint_ctx: &mut PaintCtx, geom: &Geometry) {
let bg_color = paint_ctx.state.get("bg_color");
paint_ctx.clear(&bg_color);
}
fn key(&mut self, _event: &KeyEvent, ctx: &mut HandlerCtx) -> bool {
ctx.animate(Animation::with_duration(1.0)
.adding_component("bg_color", AnimationCurve::Linear, Color::PURPLE)
);
true
}
}
```
As an added bonus, we can use this to make our animations begin from their current position; if an animation is added while another is active, we can interpolate from the existing animation's values instead of starting from the beginning or end.
## Environment
Another place where this dynamic `Value` + `Variables` pattern shines is with the concept of an 'environment'. The environment is a way of passing scoped state up the widget tree. An obvious use for this is with _theming_; we would like things like color schemes to be customizable by the application author in an easy way, while still providing reasonable defaults.
A simple approach to this is to use a `Variables` struct with a known default set of keys, which built-in widgets will use for their styling:
```rust
impl Widget for Button {
fn paint(&mut self, paint_ctx: &mut PaintCtx, geom: &Geometry) {
let bg_color = match (paint_ctx.is_active(), paint_ctx.is_hot()) {
(true, true) => paint_ctx.env().get(colors::CONTROL_BACKGROUND_ACTIVE),
(false, true) => paint_ctx.env().get(colors::CONTROL_BACKGROUND_HOVER),
_ => paint_ctx.env().get(colors::CONTROL_BACKGROUND),
};
let brush = paint_ctx.render_ctx.solid_brush(bg_color);
paint_ctx.render_ctx.fill(geom, &brush, FillRule::NonZero);
}
}
```
And then if an application author wanted to have a custom theme, they could do something like,
```rust
let mut state = UiState::new();
// ...
state.env().set(colors::CONTROL_BACKGROUND, Color::DEAD_SALMON);
state.env().set(colors::CONTROL_BACKGROUND_ACTIVE, Color::BRIGHT_PINK);
```
These custom colors would then be used by the built-in button widget, as well as any other widgets in the tree that used this mechanism and referenced those keys.
### Custom environment keys
New widgets can also participate in the environment, by adding their own keys and values when they are instantiated. This means that a custom widget can have its attributes overridden via the same mechanism as the default widgets.
## Further thoughts:
### Typed keys, if we want?
For even more type safety, we have the option of using _typed keys_. Essentially this looks like,
```rust
pub struct Key {
key: &'static str,
value_type: PhantomData,
}
impl Key {
pub const fn new(key: &'static str) -> Self {
Key {
key,
value_type: PhantomData,
}
}
impl Variables {
pub fn get>(&self, key: Key) -> V {
let value = match self.store.get(*&key.key) {
Some(v) => v,
None => panic!("No Variables key '{}'", key.key),
};
value.into_inner_unchecked()
}
}
// Example:
let vars = make_variables();
let point_key = Key::::new("my_point_key");
let my_point = vars.get(point_key);
// panics if this value is not a point
```
This is currently implemented in my experimental branches, and offers some nice guarantees; for default theme attributes these keys are pre-baked, so we can guarantee that the types are correct, and there's no possibility of accidentally having the incorrect inferred return value; you have to be explicit about what you expect when you create the key. This also encourages the practice of declaring all keys as `consts`, which is good practice.
### Keys referencing Keys
It may be that for the widget state, we want the ability to include a key that references a value in the environment itself. This would be important for animations. As a concrete example, lets say we want a button background to animate between the dynamic `colors::CONTROL_BACKGROUND` and `colors::CONTROL_BACKGROUND_ACTIVE` colors. In the buttons `paint` call, we want to have access to a `'bg_color'` state variable that will represent the _current_ color.
If we want the default for this value to be whatever is currently declared in the theme, we might want to do something like:
```rust
fn init_state(&self) -> Option {
let mut vars = Variables::new();
vars.set("bg_color", colors::CONTROL_BACKGROUND);
Some(vars)
}
```
Here the value for this key isn't itself a color, but is instead another key that resolves to a color.
### Arbitrary types in `Value`
If we wish, we can include a `Custom` variant in `Value`, which can store anything that can be `dyn Any`. We can provide a custom macro that writes the correct `TryFrom` / `Into` implementations for a custom type. I've prototyped this and have it compiling, but I want to hold back until we have a concrete use case.
### State Magic
If we get ambitious, there is some fancy macro stuff we could do make `init_state` unnecessary. I could imagine code that looked something like,
```rust
#[druid(widget)]
Button {
#[druid(state, init = colors::CONTROL_BACKGROUND)]
bg_color: Color,
#[druid(state, init = colors::BUTTON_BORDER)]
border_color: Color,
#[druid(state, init = botton::BORDER_WIDTH)]
border_width: f64,
label: Label,
}
```
It is not too difficult to imagine getting from here to something that provides some of the qualities of SwiftUI (for instance, changing a `state` variable automatically triggers an invalidation).
## Conclusion
These are preliminary thoughts, but I wanted to get a rought cut of this down so we could discuss before I dig too much more into the implementation. If there's anything I can clarify, please let me know, otherwise feedback is welcome!
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reviewing the proposed Value and Variables model, then trace the existing widget, animation, and environment architecture to determine where this design would fit. No implementation files or tests are named; done would require an agreed design and an identified implementation scope before coding begins.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- desktop, frontend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 20/100