Open s3thi opened 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. 👍
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.
I feel the current styling implementation is conceptually solid. Storing style information in Env
s instead of directly on the widgets, and using a series of nesting Env
s 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.
It would be great to have syntax-sugar for quickly wrapping widgets inside Env
s, 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 structThere have been discussions around storing enabled/disabled states for widget subtrees inside Env
s (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).
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 think we're generally on the same page:
Env
as struct: 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.
One thing to note is that the current design of Env
is incomplete. In particular, we imagine having an array of 'override slots' that sits in front of the map; this will have some fixed, small size (8/16) and when you override a key on an env, we use these slots first; only if the slots are full do we actually clone and modify the map. This will let us have the map be a shared pointer in the general case, and reduce cloning, and this optimization will also be impossible if Env
is a struct.
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.
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:
Env
entries cannot be deleted; the built-in entries, as well as any entries added by the user at launch, are guaranteed to always be available and always be the correct type, because,Env
keys are typed, and it is a hard error to try and overwrite a key with a key of a different type. So for instance env.set(theme::TEXT_COLOR, Size(50.0, 20.0))
will crash.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.
I'll go through widgets and will move non-configurable parameters to theme as in commit ^^ cc @futurepaul
@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.
@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.
@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.
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);
...
}
}
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.
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!
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?
The current
EventScope
API allows me to override the variables listed indruid::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 theEnv
, these variables appear as literals in the source code.For example, the border radius for a
Button
is 4.0. It appears indruid/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 aTextBox
has a border radius of 2.0. Both values appear as literals in the source code for those widgets. Exposing border radius values indruid::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.