You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
Each of these concrete types implements Into<Value>, and TryFrom<Value> (we may wish to use a custom trait in place of TryFrom, but it works for prototyping):
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:
pubstructVariables{store:HashMap<String,Value>,}implVariables{pubfnget<E,V:TryFrom<Value,Error = String>>(&self,key:&str) -> Result<V,String>{matchself.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:
traitWidget{fninit_state(&self) -> Option<Variables>{}}structBouncingBall;implWidgetforBouncingBall{fninit_state(&self) -> Option<Variables>{letmut vars = Variables::new();
vars.set("ball.ypos",0.0);Some(vars)}fnpaint(&mutself,paint_ctx:&mutPaintCtx,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 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.
// on key down we create a new animation that includes a color and a floatfnkey(&mutself,_event:&KeyEvent,ctx:&mutHandlerCtx) -> 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.fnanimate(&mutself,anim:&Animation,ctx:&mutHandlerCtx){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:
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:
And then if an application author wanted to have a custom theme, they could do something like,
letmut 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,
pubstructKey<T>{key:&'static str,value_type:PhantomData<T>,}impl<T>Key<T>{pubconstfnnew(key:&'static str) -> Self{Key{
key,value_type:PhantomData,}}implVariables{pubfnget<V:TryFrom<Value,Error = String>>(&self,key:Key<V>) -> V{let value = matchself.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::<Point>::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:
fninit_state(&self) -> Option<Variables>{letmut 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,
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!
The text was updated successfully, but these errors were encountered:
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:
Each of these concrete types implements
Into<Value>
, andTryFrom<Value>
(we may wish to use a custom trait in place ofTryFrom
, but it works for prototyping):To use these values, we put them in a map, and access them with a special getter method:
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:
The main motivation for this pattern is to allow drawing code to be animation aware.
With animations:
In the animate 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.
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:
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:And then if an application author wanted to have a custom theme, they could do something like,
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,
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
andcolors::CONTROL_BACKGROUND_ACTIVE
colors. In the buttonspaint
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:
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 inValue
, which can store anything that can bedyn Any
. We can provide a custom macro that writes the correctTryFrom
/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,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!
The text was updated successfully, but these errors were encountered: