linebender / druid

A data-first Rust-native UI design toolkit.
https://linebender.org/druid/
Apache License 2.0
9.56k stars 568 forks source link

Expose a larger number of design tokens in druid::theme #420

Open s3thi opened 4 years ago

s3thi commented 4 years ago

The current EventScope API allows me to override the variables listed in druid::theme to customize how my application looks. However, there are a large number of variables used implicitly in the default widgets that I can't override. Instead of being picked up from the Env, these variables appear as literals in the source code.

For example, the border radius for a Button is 4.0. It appears in druid/src/widget/button.rs, but I can't retrieve or override it because it's just a literal value.

Finding such variables and making them explicit in druid::theme will make styling a Druid application easier. There is precedent for this in the Design Systems communities, where these kinds of atomic base units are called design tokens. This article is a decent explainer, and this is a list of tokens used in the Salesforce design system.

Adopting this approach would reduce inconsistencies across the toolkit and makes design decisions more explicit. E.g, Button has a border radius of 4.0, but a TextBox has a border radius of 2.0. Both values appear as literals in the source code for those widgets. Exposing border radius values in druid::theme would've either prevented this inconsistency, or documented it as an explicit design decision.

This would also make it easy for custom widgets to fit in with the default set widget set.

It might not be an explicit goal of Druid, but this approach would also allow radical re-skinning of applications, with variable changes affecting both default, custom and community-built widgets.

I'm sure there are more ideas that Druid could pick up from the Design Systems community, but I can't think of anything at the moment. For now, setting up explicit design tokens would be a great start.

cmyr commented 4 years ago

Cool, I think there's two separate related things here but they're both basically a matter of "haven't gotten to it yet":

Thanks for the issue and the links, it looks like a good starting place. 👍

s3thi commented 4 years ago

I've been thinking a bit more about this, but I feel I need to work with the toolkit a bit more before patterns start to emerge. Meanwhile, I'll continue using this issue as a place to do a braindump of styling-related ideas, in case someone wants to pick this up at some point.

General architecture

I feel the current styling implementation is conceptually solid. Storing style information in Envs instead of directly on the widgets, and using a series of nesting Envs to override styles for specific parts of the subtree are both great ideas. They remind me of CSS-in-JS implementations. I'm not a fan of them on the Web, but I feel that those patterns make sense in native toolkits.

Syntax sugar

It would be great to have syntax-sugar for quickly wrapping widgets inside Envs, but I feel that's a discussion for later. I imagine it could look something like this:

let inverted_label_style = style!{
  "text-color" => Color::white(),
  "background-color" => Color::black()
};
let styled_label = Label::new("A styled label").style(inverted_label_style);

Syntax inspired by CSS and maplit.

Env should be a struct

There have been discussions around storing enabled/disabled states for widget subtrees inside Envs (see #143). If that's going to be the actual API, it would make sense for Env to be a plain old struct with a standard set of fields (such as style and enabled). The fields could be Rust types or custom types depending on the functionality they represent.

In light of this, it makes sense to have some kind of a Style type that contains the functionality that Env currently has. This type could be responsible for other styling-related tasks, such as loading styles from a file (which could just be CSS, or a restricted subset of CSS).

Complex value types

It looks like Env can store complex types like LinearGradient and Rect, but the current theme isn't using those. Button, for example, manually creates a LinearGradient in its paint() using the BUTTON_LIGHT and BUTTON_DARK values.

From what I understand, this could directly be stored on the Env as a LinearGradient. In general, we will eventually need to store more complex data types for styling (such as paths, shadows, or transitions).

cmyr commented 4 years ago

I think we're generally on the same page:

s3thi commented 4 years ago

The motivation for env to be a map is that it allows an application to define a bunch of custom style keys or other fields that have initial values, but which are then over-ridden at various points in the tree.

My concern with this is that Env might end up becoming a kind of stringly-typed dumping ground for everything that needs to be shared across the widget tree, with no way for the user of the library to know which keys they can expect to find on it.

A good design, IMHO, would define a standard set of fields that can be expected to always be present, and also allow one field that could contain arbitrary data. For example:

struct Env {
    style: SomeKeyValueStoreType,
    enabled: bool,
    user_data: SomeKeyValueStoreType,
    ...
}

However, I understand your concern @cmyr. Having this struct would result in a lot of cloning everywhere. I'm wondering if there's a way to have things both ways, but my Rust skills are not quite good enough to get there yet.

cmyr commented 4 years ago

So I think that we fundamentally agree, and Env actually is our attempt to have it both ways, and was inspired by basically identical concerns to yours.

The contents of Env should be 100% known at all times; it will either be items that are declared and documented in druid, or it will be items that have been explicitly added by the framework user. In addition, Env has some interesting properties (enforced by the API) that make accidental surprises unlikely during use:

The goal, is basically that Env should have all of the guarantees of a struct, but allow arbitrary new fields, and should existing fields to be overwritten at various points in the tree, assuming the new and old types are the same. For instance: It is intended that the user can add arbitrary items to their env; for instance if they are writing (say) a music notation editor, they might have a NOTE_COLOR key, which they overwrite at various places in their widget graph to change how notes are drawn.

Dmitry-Borodin commented 4 years ago

I'll go through widgets and will move non-configurable parameters to theme as in commit ^^ cc @futurepaul

s3thi commented 4 years ago

@Dmitry-Borodin thanks a lot for taking this up. I've been building an app where I want the buttons to have a more "flat" look than they have by default, and the lack of some of these configurable tokens was preventing me from building what I wanted.

Just a note on your commit: I feel it would make more sense to have a BORDER_RADIUS_LARGE and BORDER_RADIUS_SMALL instead of BUTTON_RADIUS and TEXTBOX_RADIUS. This kind of naming is what you usually see in design systems for the web.

More generic names for design tokens encourages reuse, which then makes it easier to override the look of multiple widgets by changing a small number of tokens. It also establishes a hierarchy of measures where the variables in the hierarchy can be configured such that they are mathematically related to each other.

For example, in my app I could could force the margins between widgets as well as the sizes of widgets to be whole number multiples of 4. This would result an implicit grid that all widgets would line up with, thus producing a UI that feels more balanced to look at.

Dmitry-Borodin commented 4 years ago

@s3thi sorry I probably didn't get what do you mean. Currently widget takes some predefined values from theme by key, and the key for button starting with "button", because I may define it bigger or smaller than for textbox in my app. This is the name for key, not for default value.

For example if I want textbox radius to be bigger than a button. If I would rename those keys to big and small, I would have to set value for BORDER_RADUIS_LARGE smaller, than for BORDER_RADUIS_SMALL, which is misleading.

I like the idea of adding "border" to the key name for clarification.

s3thi commented 4 years ago

@Dmitry-Borodin I wasn't clear enough in my comment. Apologies.

I opened this issue because I felt it would be useful if Druid's styling system was inspired by the "design systems" concept that is popular in UX design right now.

When you build a design system for an app, you start with a basic set of values and measures called design tokens. This can include colors, font styles, font sizes, margins and paddings, opacities, shadows, etc. These common values are shared across all the widgets in the design system. For an example of what that looks like, see https://www.lightningdesignsystem.com/design-tokens/#category-radius

These design tokens are defined first, and the widgets are designed around them. That is, instead of building a TextBox, Button, and Slider first and then extracting common values for colors, border, etc. from them, you define the common values first and build your widgets around those common values. Of course, there will be cases when those common values won't make sense, but the idea is to keep the number of those exceptional cases as small as possible.

This approach lets you radically change the look and feel of your UI by changing a very small number of variables. You could make a design more compact by simply changing three or four padding values, or you could make a design more or less rounded by simply changing two or three radius values. This gives developers a lot of freedom to experiment with design without having to change too much code.

So, instead of extracting PROGRESS_BAR_RADIUS, TEXTBOX_BORDER_RADIUS, and BUTTON_BORDER_RADIUS from the code, we could define more generic values for border radius. We could call these values BORDER_RADIUS_SMALL, BORDER_RADIUS_MEDIUM, and BORDER_RADIUS_LARGE.

The value of BORDER_RADIUS_SMALL could be 2.0, BORDER_RADIUS_MEDIUM could be 4.0, and BORDER_RADIUS_LARGE could be 8.0. In the paint method of Button, ProgressBar, and TextBox, we could use one of those generic values instead of using specialized values for each widget.

Future widgets could be designed around those three values, and defining new values would be restricted to very special cases.

If a user of Druid decides they want a more angular design for their app, they could reset all of these values to 0.0. Just changing those three values would radically change the look of the entire app. This would make experimentation easier, and developers would find it easier to customize their applications.

I hope this made more sense than my previous comment.

sysint64 commented 4 years ago

I feel like Env is a more low-level abstraction. The current approach to store various theme parameters in there is the right decision; we can easily adjust how a particular widget looks. On top of Env, you can develop more high-level abstraction like a theme, which would change all env keys for widgets, e.g., you can create a struct with parameters as @s3thi suggested.

Something like this:

struct MyTheme {
    pub border_radius_small: f64,
    pub border_radius_medium: f64,
    pub border_radius_large: f64,
    ...
}

impl MyTheme {
    pub fn apply(&self, env: &mut Env) {
        env.set(theme::BUTTON_BORDER_RADIUS, self.border_radius_medium);
        env.set(theme::SCROLLBAR_RADIUS, self.border_radius_small);
        env.set(theme::PROGRESS_BAR_RADIUS, self.border_radius_small);
        env.set(theme::TEXTBOX_BORDER_RADIUS, self.border_radius_medium);
        ...
    }
}
sysint64 commented 4 years ago

What I don't like in current approach - it is how the library is rendering widgets, I like how flutter managed this, there I can make my buttons, text boxes, and other widgets look how I want. I would like to have control over how my widgets look, I would like to implement paint from scratch, but I don't want to reimplement all event handling and layout measurement and so on.

cmyr commented 4 years ago

I definitely think there's lots of room for us to find new and better patterns for this stuff. At least with buttons, we've moved in this direction; you can use the Painter widget + on_click() to have a custom clickable widget, and you don't need to implement any fancy logic. More experimentation in these directions is definitely welcome!

Misaka299 commented 2 years ago

Is there any progress in this discussion?

I think @s3thi's suggestion is good.

Complex value types It looks like Env can store complex types like LinearGradient and Rect, but the current theme isn't using those. Button, for >example, manually creates a LinearGradient in its paint() using the BUTTON_LIGHT and BUTTON_DARK values.

From what I understand, this could directly be stored on the Env as a LinearGradient. In general, we will eventually need to >store more complex data types for styling (such as paths, shadows, or transitions).

I am currently working on a project of my own. When using the button, I want the button to be a solid color, the activated state is another solid color, and the disabled state is also another solid color. His suggestion, if implemented, should allow me to set the theme of the button in an elegant way.

My rust is not very good. Currently I want to implement my button theme. It can only be implemented locally through the clone repository, local reference, and modification of the local druid source code. Or is there a better way to achieve it?