Replies: 3 comments 5 replies
-
Please don't use JSON for anything that a human is supposed to write. That's a machine serialization format, not a source code format. It doesn't have comments, is too verbose, serde round-trip doesn't preserve whitespaces, etc. TOML or YAML are infinitely better at being configuration files, and both can be serialized/deserialized by serde just as well. Or just use a subset of CSS for this. Surely, writing CSS parser with serde framework isn't that hard... |
Beta Was this translation helpful? Give feedback.
-
So I recently worked on a styling lib for a silly experiment trying to recreate the cyberpunk main menu. This styling moduleIt's a rust module: https://github.com/nicopap/cuicui_layout/blob/3eea65e7975ac73242e977011696e592ae3e11b9/examples/chirpunk/src/style.rs It relies on two types
|
Beta Was this translation helpful? Give feedback.
-
I don't really want this thread to get bogged down in issues of file formats for stylesheets, that's just syntax. I actually don't care all that much what format gets chosen. To me, the more interesting question is: "What subset of CSS semantics should we support?". For some existing UI libraries - for example bevy_egui - the answer is, "nothing". The appearance of the widgets are implemented in Rust code, and cannot be changed without effectively re-implementing the widget. I think this is a non-starter for game authors, because games require novelty in their UIs. Other libraries offer a smidgen of customization options - you can perhaps change the background image nine-patch of a button, but all of the other attributes such as alignment are predetermined. Again, I don't think this is enough. On the other hand, there are libraries which try to offer the full set of styling options one would get running in a browser. I think that's too much - both because of the difficulty of implementing it, and because (as I explained above) some of those options lead to code that's hard to maintain. So the challenge is to cherry-pick the bits we like, and adapt them to the set of design patterns that live in the Bevy ECS and Asset worlds. The model I am thinking of is one where styles are assets, that is, assigning a style to a widget is basically assigning an asset handle of a style resource to a property of the widget. But I don't want to throw out CSS rule-matching entirely, because rule-matching is useful for dynamic states (hover, pressed, focused, primary vs. secondary and so on). Also, I think it's useful to be able to compose styles, that is, to be able to apply a combination of multiple style assets to a single widget. Here's a sketch of a selector matching scheme, represented as an enum: #[derive(Debug, PartialEq, Clone)]
pub enum Selector {
/// If we reach this state, it means the match was successful
Accept,
/// Match an element with a specific class name and proceed to the next state.
Class(String, Box<Selector>),
/// Reference to the current element.
Current(Box<Selector>),
/// Reference to the parent of this element.
Parent(Box<Selector>),
/// List of alternate choices.
Either(Vec<Box<Selector>>),
} The enum represents the nodes of an NFA. Each node has a pointer to the next node in the chain. You start at the start node ("Current") and try to get to the "Accept" state. If you do, the selector matches, otherwise it doesn't match. So for example, the CSS selector expression:
Note that except for the I deliberately left out the "ancestor" matcher which in CSS has the syntax "A B", that is, two selectors separated by a space. The reason for doing this is because it requires backtracking (because you don't know how far up the ancestor chain you have to search), and because it's not necessary, you can use multiple parent selectors if you really need to. For the same reason, the "current" node always has to come first, meaning that the search always goes up the ancestor chain, never down. Again, this is to avoid backtracking and searching over children. In fact, the There's also no matches for pseudo-classes like |
Beta Was this translation helpful? Give feedback.
-
I've been busy prototyping some ideas around widget styling, and I wanted to share some of my thinking. While this is mostly going to be a description of what I'm building, it's also meant to be suggestions for the people thinking about the next steps in the bevy_ui evolution.
Why stylesheets?
One question that might be asked is, why bother with styles as an asset? Why not just define styles in Rust code?
The main advantage of stylesheets is that it allows the development of widget behaviors and widget styles to be done independently. A user can pull in a library of widget behaviors, and a library of styles, whose authors have never met or are even aware of each other.
In the web development world, there is a robust ecosystem of widget libraries such as Material UI, ANT Design, Bootstrap, Semantic UI, and at least three dozen others whose primary value is their aesthetics. The actual behaviors of these libraries are remarkably similar, because they conform to commonly accepted standards of UI design and accessibility. But each has its own distinctive look, which can vary from professional, to friendly, to just plain funky.
In the game world, user interface novelty is a virtue - it's expected each game has widgets that look slightly different. However, this does not mean that the behavior of those widgets should be dramatically different, and especially it does not mean that game creators should be encouraged to violate accessibility guidelines. Particularly for complex widgets such as modal dialog boxes, drop-down menus, radio buttons and others, it can be tricky to get accessibility just right. Game authors should not have to be burdened with the task of re-inventing combo boxes or rich text editors.
Therefore, I believe it should be a goal to encourage an ecosystem where widget authors can publish "white-label" crates which offer robustly engineered widgets with no-built-in styling, and allow other creators to add their own styles on top of those widgets.
Why not CSS?
There are a lot of good things about CSS. However, in recent years the web world has become aware that some aspects of CSS are flawed, in the sense that they increase the project's maintenance burden. For example, the rule-based nature of CSS selectors means it is not possible to write an automated linter that can detect dead CSS code, and I have personally seen how unused CSS can accumulate in a large codebase.
If you take a look at some of the most popular CSS tooling on the web (CSS modules, Web components, CSS-in-JS frameworks and so on), these tools go out of their way to turn certain features of CSS off. Coincidentally, these features (such as the CSS cascade and complex selector matching) are among the most difficult to implement, and have the highest negative impact on performance. So there are multiple reasons to leave these features out from a Bevy implementation.
(Background: I'm somewhat familiar with this area because in 2007 when I worked at Electronic Arts, I wrote an HTML/CSS browser that ran on Windows, XBox and PS/2 and used hardware-accelerated drawing - it was used in Spore.)
The approach that I'm taking in my current work is inspired by the TypeScript library vanilla-extract, which is a compile-time CSS-in-JS framework that lets you define CSS rules in strongly-typed TypeScript. This "language" is isomorphic to CSS, meaning that you have access to all of the familiar CSS properties and values, but the syntax is slightly different.
Vanilla-extract allows CSS selector matching, but in a restricted form. The restriction is that style rules can only be applied to the element they are attached to - you cannot write a generic rule that applies to all children, or to all nodes of a given type. You can, however, write rules that are conditional based on the element classes, as well as the classes of the element's parent. So for example, this syntax is allowed:
.widget.disabled > &
But this syntax is not:
&.widget > .disabled
(
&
means "current element" and is borrowed from SASS.)I've written a number of widget libraries using vanilla-extract (one for React and one for Solid) and I've not found this restriction to be terribly burdensome. Also, one nice thing about this restriction is that the matching algorithm can be implemented in Rust/Bevy very efficiently.
I've also decided to use JSON as the host language for specifying styles.
Why JSON?
My first attempt at doing this actually used XML, but I later switched to JSON. The main advantage of JSON over something like RON is interactive schema validation in VSCode. For stylesheets that are to be authored by hand, I want precise and fine-grained validation of every property in the editor.
Another advantage of JSON (compared to regular CSS stylesheets) is that it can be decoded by serde (with some help from willow), which means that it can also be serialized again. This may be important for writing an interactive editor.
The current prototype supports the set of style attributes that bevy_ui supports, such as "background-color", "flex-direction" and so on. It also supports some shortcuts which are directly inspired by vanilla-extract:
"min-width": "7px"
and"min-width": 7
are equivalent.UiRects
properties can specify one to four lengths, e.g. "1px 7px auto 3vmin". The order is the same as CSS (top, right, bottom, left), and they default if fewer numbers are given. (I'm considering also allowingUiRects
to be written using array syntax, e.g."margin": [0, 0, 10, "12px"]
)."#ff0000"
or"rgba(255 255 255 / 0.5)"
.However, there are some differences from CSS:
Asset references: Properties such as "background-image" which would normally accept a URL in CSS (via the
url()
function), instead accept an asset key:"background-image": "asset(../images/button.png)"
The deserializer recognizes the "asset" syntax and knows to load this as a resource handle. Note that relative paths are supported.
Selectors use the same scheme as vanilla-extract, where the selectors are map keys:
Note that Bevy widgets don't have built-in dynamic states such as
:hover
or:focus-within
, so any dynamic styles have to be done through regular class names.Applying Styles
The internal representation of style objects uses a sparse key/value system, so you don't get a giant struct with a bunch of
Option
s, most of which are set toNone
. During style evaluation, a style asset is "merged" into aComputedStyle
which is just a struct that contains a bunch of Bevy style structs (Style
,BackgroundColor
, etc.) This is then applied to the widget components using a customCommand
.Selector Matching
The algorithm for selector matching is very performant - it's a simple NFA (Non-deterministic Finite Automata) that takes in an element as its input, and returns true or false if there's a match. The only information it needs is the parent/grandparent chain and the list of classes associated with each element. Unlike regular CSS selectors, there's no need to walk the whole widget tree globally in response to an event.
Evaluating CSS variables.
Individual CSS attributes are parsed into a simple expression language, an AST. For attributes which are constants like
"flex-direction": "ltr"
, the attributes are pre-optimized into their bevy_ui enum equivalents, e.g.bevy::u::Direction::LeftToRight
. However, attributes which contain a variable reference are left in AST form and can be evaluated when the widget style is updated.Another optimization idea I've been considering is to pre-compute a bitmask of which widget style properties are dynamic and which are constant (by looking at the selectors and attributes), and only re-evaluating the dynamic ones in response to an event. However, this may be premature optimization on my part.
Relative asset labels
Imagine a widget such as a slider which has multiple internal parts: a track, a thumb, and so on. Each of these elements needs to have a style, and because we don't allow parents to style their children, each one needs a reference to a style asset.
Unfortunately, we also want the style to be a parameter so that the same slider can be styled differently in different contexts. It would be cumbersome if we had to pass in a separate
AssetPath
for every slider part.The solution comes in the form of relative labels. If you have ever worked with json-schema, you'll be familiar with JPointer syntax, which allows relative paths from within a document such as
#./abc/def
. The#
at the beginning tells us this is a fragment identifier, meaning that it's not a filesystem path, but rather a reference to the internal structure of the document that is relative to the current asset key.We can design our slider to take a "base asset path" as a parameter, and then have the individual parts use relative paths:
#./slider-track
,#./slider-thumb
and so on. So long as our style asset structure conforms to this naming scheme, it will work with any style asset.Show me the code!
Unfortunately, the current prototype requires Bevy Assets V2 and is still in development. I hope to have something to show in a few weeks.
Beta Was this translation helpful? Give feedback.
All reactions