Styles and Themes in Gooey

This thread is inspired by the work being done in this pull request:

Styling is a complicated beast to tame. In my previous attempts in Kludgine, I liked parts of how I designed the styling and theming, but other parts I disliked. It’s kind of funny, I know for a fact, one of the concepts I reimplemented in stylecs I had previously removed from Kludgine. I don’t remember why I removed it from Kludgine originally, so a part of my brain is still worried I’m going to rediscover some reason this was a bad idea.

To adequately describe how styling is working, I want first to take a walkthrough of the crate that I extracted and refined from Kludgine and then give a primer on how Gooey uses the crate.

Introducing stylecs

stylecs is short for Style Component System. It is an attempt at providing a minimalistic approach to the data structures needed to implement extensible styling in any application. It doesn’t directly contain any knowledge of fonts, colors, measurements. If you adopt stylecs directly into your application, you will be responsible for defining the style components your application will use.

At its core is the Style type. If you’ve used an Entity Component System, the interface should look fairly similar to an “entity” in most APIs. You can add components and get components based on their static type.

Each component must implement the StyleComponent trait. A style component is defined with two bits of behavior: should it be inherited in a parent/child context, and what happens when merging two Styles with the same component.

Inheriting Styles

Style provides the merge_with() function, which takes a parameter denoting whether the merge should be considered inheritance. This is useful, for example, in a fashion similar to CSS’s border-* rules: When you set the border of a DOM element in CSS, the child DOM elements don’t inherit the border values. However, when you set the font family on a DOM element, the children inherit that value.

The decision with stylecs was to use the Rust type system to our advantage and allow each StyleComponent to dictate how it is meant to be handled.

For example, here’s how

Merging Components

StyleComponents are meant to be immutable. Still, there are some situations where you might want to merge two Style instances and have the merge operation produce a merge of the contained component. By default, components will overwrite each other based on the order in which they are merged/inherited. However, a component can provide an implementation for merge() to customize the behavior.

Style Component Hierarchies

One of the challenges faced in Gooey is that I don’t want a one-to-one mapping with DOM elements. For example, I might have a FormInput widget with a label and a text edit control. As the consumer of my widget, you have the ability to style FormInput as a whole, but not the individual widgets FormInput is constructing. This is different than in CSS – you can write a more specific selector to target nearly any DOM node.

The proposed solution to this is FallbackComponent. The FormInput widget could expose new style components: FormLabelColor and FormEditorColor. They should have sensible defaults, however, so they implement FallbackWidget using TextColor as its Fallback.

At the point of applying styles, the widget can use style.get_with_fallback::<FormLabelColor>(). If style contains FormLabelColor, its value will be returned. If not, but it contains TextColor, the text color will be returned. Finally, if it has ForegroundColor, that value would be returned. If not, None is returned.

You can see it in action in the Kludgine rasterizer looks up the TextColor component:

options.get_with_fallback::<TextColor>()

StyleSheets

The StyleSheet type is a work in progress – I only began using it in Gooey over the last couple of days.

At the core is the Rule type which offers various filtering options for when to apply the Style contained in the rule. The evaluation is very simple and straightforward – I fear that it will need to become more sophisticated to properly style full applications.

StyleSheets can also be merged, allowing for a user style sheet to be evaluated on top of a style sheet provided by a theme.

How styling in Gooey will work

The module gooey::core::styles re-exports parts of stylecs but also provides some basic style components. Each of the built-in widgets will use these core style components and build atop them. Third-party widgets are expected to use these core style components directly or through the FallbackWidget trait.

The core colors for the built-in StyleSheet are hardcoded right now. I want to abstract this list of colors into some sort of color palette structure. The goal would be to allow you to re-colorize an application by providing a new palette.

Of course, if you want to provide your own StyleSheet and not use any built-in rules, Gooey will support that workflow too.

The last thing I want to show off is gooey/examples/basic.rs:

Container::from_registration(builder.register_widget(
    CounterWidgets::Button,
    Button::new(
        "Click Me!",
        builder.map_event(|_| CounterEvent::ButtonClicked),
    ),
))

This chunk is how the example creates a button within a container with an event callback for when the button is clicked. What if you wanted to change the button color?

Container::from_registration(builder.register_widget(
    CounterWidgets::Button,
    Button::new(
        "Click Me!",
        builder.map_event(|_| CounterEvent::ButtonClicked),
    )
    .with(ButtonColor(/* ... */)),
))

I’m very happy with how concise the widget setup and styling API is turning out.

What’s next?

The pull request will be updated with a ToDo list as I start reigning in my vision. I try to keep pull requests focused, and this one has an opportunity to grow unwieldy if I don’t set some goals soon.

The next missing piece is that all of this style information isn’t handled in the browser implementation. I have a few ideas on how I’m going to approach this. It’s not actually a tough problem per se. Rather, it would be easy to implement inefficiently.

This link is now broken, as the type was moved into gooey-core today. In the end, I realized that Transmogrifiers needed to take an active role in the process of converting style sheet rules into CSS. There’s a lot of thought into this, but I’m trying to keep tonight’s update a little shorter than my usual novellas.

To boil it down to a simple use case, imagine you’re creating a SignInComponent, encapsulating the elements needed to sign into an app. On it, you have a few different text fields, buttons, labels, and maybe more. A consumer of this SignInComponent would potentially want to customize the sign-in button’s color. The component author could allow this by defining a SignInButtonColor StyleComponent, which uses a hypothetical ButtonColor component as a FallbackComponent, which ultimately falls back on BackgroundColor.

The SignInComponent needs to be actively involved in translating rules to CSS because it can be the only type that knows how to use SignInButtonColor.

This realization was one of the final pieces that fell into place, but now we have clicky buttons that are the same color on both Kludgine and in the browser:

The background color of the window is not correct in either the browser or the app – although technically the browser is closer to the correct result. It’s still a TODO to have a root background color.

How Gooey is applying styles in the browser

The above refactoring was one of the keys to figuring out how it all fits together, but let’s take a step back and try to understand the problem. If you read yesterday’s update, I ended with this note:

One way to implement styling would have been to add events to hover/activate/focus events and let the WebSysTransmogrifiers update the style in real-time. While it sounded neat and very powerful, it just felt like a departure from the hopes I had for the DOM integration. It sounded messy.

The focus today was to implement a system where the WebSys frontend could manage style rules centrally and convert StyleSheets into actual CSS rules in a <style> tag in the DOM. It uses the CssStyleSheet API, which was a neat discovery. This means the DOM is left nice and clean, and the style rules are applied by the browser:

There’s still a lot of work to go before I consider this done, but I’m really excited at the results of today’s work.