Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

An initial design for widget state, environment, and animation #53

Open
cmyr opened this issue Jun 17, 2019 · 0 comments
Open

An initial design for widget state, environment, and animation #53

cmyr opened this issue Jun 17, 2019 · 0 comments
Labels
discussion needs feedback and ideas write-up Detailed information / thoughts

Comments

@cmyr
Copy link
Member

cmyr commented Jun 17, 2019

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:

#[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<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:

pub struct Variables {
    store: HashMap<String, Value>,
}

impl Variables {
    pub fn get<E, V: TryFrom<Value, Error = String>>(&self, key: &str) -> Result<V, String> {
        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:

trait Widget {
    fn init_state(&self) -> Option<Variables> {}
}

struct BouncingBall;

impl Widget for BouncingBall {
    fn init_state(&self) -> Option<Variables> {
        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 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 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:

impl Widget for BouncingBall {
    fn init_state(&self) -> Option<Variables> {
        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:

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,

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,

pub struct Key<T> {
    key: &'static str,
    value_type: PhantomData<T>,
    }
    
    impl<T> Key<T> {
        pub const fn new(key: &'static str) -> Self {
        Key {
        key,
        value_type: PhantomData,
    }
}

impl Variables {
    pub fn get<V: TryFrom<Value, Error = String>>(&self, key: Key<V>) -> 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::<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:

fn init_state(&self) -> Option<Variables> {
    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,

#[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!

@cmyr cmyr added the discussion needs feedback and ideas label Jun 17, 2019
@luleyleo luleyleo added the write-up Detailed information / thoughts label May 21, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion needs feedback and ideas write-up Detailed information / thoughts
Projects
None yet
Development

No branches or pull requests

2 participants