emilk / egui

egui: an easy-to-use immediate mode GUI in Rust that runs on both web and native
https://www.egui.rs/
Apache License 2.0
21.36k stars 1.54k forks source link

CSS-like styling #3284

Open emilk opened 12 months ago

emilk commented 12 months ago

Some half-finished ideas around how to improve the styling and theming story for egui.

Background

Styling for egui is currently supplied by egui::Style which controls spacing, colors, etc for the whole of egui. There is no convenient way of changing the syling of a portion of the UI, except for changing out or modifying the Style temporarily, and then changing it back.

We would like to have a system that can support CSS-like selectors, so that users can easily style their ui based on the Style Modifiers (see below):

It would be very beneficial if such styling could be set in a single text file and live-loaded.

Action plan

Proposal

Style modifiers

Here are some things that could influence the style of a widget:

For instance, a user may want to change the sizes of all buttons within the "settings_panel".

The per-Ui identifier would need to be a hierarchial stack, so the query to a theme would be something like:

Give me the WidgetStyle for a ****button**** that is **hovered** that is nested in a “options”→”internals”

We could also consider having dark/light mode as a modifier, allowing users to specify both variants in one theme file.

WidgetStyle

Let’s start with this:

pub struct WidgetStyle {
    /// Background color, stroke, margin, and shadow.
    pub frame: Frame,

    /// What font to use and at what size.
    pub text: TextStyle,

    /// Color and width of e.g. checkbox checkmark.
    /// Also text color.
    ///
    /// Note that this is different from the frame border color.
    pub stroke: Stroke,
}

pub struct TextStyle {
    pub font: FontId,
    pub underlined: bool,
    …
}

If each widget as given a WidgetStyle it could then use it both for sizing (frame margins and font size) and its visual styling. The current theme would select a WidgetStyle based on some given style modifiers, and its interaction state (computed at the start of the frame, thanks to https://github.com/emilk/egui/issues/3936).

WidgetStyle would be used by all built-in widgets (button, checkbox, slider, …) but also each Window and Ui.

Example

fn button_ui(ui: &mut Ui, text: &str) {
    let id = ui.next_auto_id(); // so we can read the interact state
    let style = ui.style_of_interactive(id, "button");
    let galley = ui.format_text(style, text);
    let (rect, response) = ui.allocate(galley.size + style.margin.size);
    style.frame.paint(rect, ui);
    style.painter().text(rect, galley);
}

Speed

We must make sure egui isn’t slowed down by this new theming. We should be able to aggressively cache the WidgetStyle lookups based on a hash of the input modifiers.

Theme plugins

We could start by having a plugin system for the theming, something like:

trait ThemePlugin {
    fn widget_visuals(&self, modifiers: &StyleModifiers) -> WidgetStyle;
}

We could then start with a simple rule engine, but still allow users to implement much more advanced ones (e.g. more and more CSS-like).

Rule-engine

Eventually we want a fully customizable sytem where rules set in one theme file will control the look of the whole UI. Such a rule system has a few open questions to resolve:

Rules

The rules can apply partial settings or modifiers. For instance, a rule can set the font and increase the brightness of the text.

Exactly how to specify the rules (i.e. in what language) is outside the scope of this issue, but here is a few examples of the kind of rules one could maybe want to do:

button hovered: {
    stroke.color.intensity: +2
}

// Make disabled things less bright:
disabled: {
    frame.fill.intensity: -2
    stroke.color.intensity: -2
}

// Make hovered interactive widgets brighter:
interactive hovered: {
    frame.fill.intensity -2
    stoke.colors.intensity: -2
}

small: {
    text.size: -2
}

heading: {
    text.size: 20
}

code: {
    text.font: "monospace"
    frame.fill: "gray"
}

weak: {
    frame.fill.intensity: -2
    stoke.colors.intensity: -2
}

strong: {
    frame.fill.intensity: +2
    stoke.colors.intensity: +2
}

hyperlink: {
    stoke.colors.intensity: "blue"
    text.underlined: true
}

window: {
    frame.fill: "gray" // wait, this will add fill for all children of windows!?
}

Color palette

We also need a color palette, indexable by brightness and opacity

https://www.radix-ui.com/colors/docs/palette-composition/understanding-the-scale

// Color modifiers
intensity +2  // modify
opacity   50% // set

In the GUI code users should be able to refer to colors both using aliases (”blue”, “header”, …) and hard-coded colors (#ff0000).

Dark mode vs light mode

We should also consider supporting both light and dark mode within the same theme. That is, one theme file should be able to set both a dark and a light theme. Perhaps “dark” and “light” is just another style modifier?

abey79 commented 12 months ago

Re: dark mode vs. light mode, I believe the heavy lifting is done by just swapping the corresponding Radix color tables. The "coordinates" (tint, index) can remain the same.

image image
chris-kruining commented 10 months ago

I am probably spewing a stupid idea here. but Dioxus is implementing CSS for native rendering. maybe it is worth seeing if you could either straight up use that, or bundle your dev power and make a generic lib that would work for both. I do realise this is very optimistic, probably even naive. But just wanted to have shared the thought

jmetz commented 10 months ago

Ah my bad - I see they have experimental WGPU support now via their Blitz renderer.

Original comment

@chris-kruining - as far as I can tell Dioxus isn't actually native, right? It's webview based : https://dioxuslabs.com/learn/0.4/getting_started/desktop#desktop-overview

chris-kruining commented 10 months ago

Ooh my bad if I got that wrong, I seem to remember the dude in the video saying "building a browser is hard" when he talked about css. So I made the presumption that they were implementing there own rendering and not just a webview.

https://youtu.be/aSxdmXjZutI?si=zmXi9mPbuFna4L6t&t=1690

ElhamAryanpur commented 9 months ago

Love this idea!

Personally faced a lot of inconvenience when trying to style individual widgets in the past, so this would be amazing!

Is there any roadmap for this or is it still in idea phase?

This and RTL support are gonna be dream come true

aspiringLich commented 7 months ago

I'm interested in writing a parser for the style language / rule engine / css clone thing. The following is a (hopefully) thought-out attempt to fill holes in the original proposal:

👉 Expand Proposal


## Style Language I would make the rule engine (which will henceforth, in this document, be referred to as the style language) closer to CSS. Mostly, this is because it reduces the learning curve (I don't think it's controversial to say a lot of people know CSS). I do like accessing properties with the dot syntax as it makes the syntax of the style language agree with rust's. ### CSS Selectors ```js // original proposal button hovered: { stroke.color.intensity: +2 } disabled: { frame.fill.intensity: -2 stroke.color.intensity: -2 } interactive hovered: { frame.fill.intensity: -2 stoke.colors.intensity: -2 } ``` ```css /* Just remembered CSS doesn't have single line comments :-( */ /* this proposal: */ button:hover { stroke.color.intensity: +2 } :disabled { frame.fill.intensity: -2 stroke.color.intensity: -2 } :interactive:hover { frame.fill.intensity: -2 stoke.colors.intensity: -2 } ``` Any "built-in" selectors that are not dynamic like `:hover` or `:disabled` have, you guessed it, a colon in front of them. This would include interaction state, and text modifiers. Selectors for widgets and custom-styled elements are written differently. Widgets are sort of like HTML elements if you squint really hard, so I think having them be written plain (e.g. `button`) is fine. Likewise, per-widget or per-ui identifiers are like `id` in HTML (that's crazy), so they could have a `#` before them. Dark / Light theme could simply be a `:dark` or `:light` selector anywhere. ### CSS Combinators Taking the next logical step, we could implement CSS combinators, which would solve the question of what the rule applies to: ```css /* just the element */ button /* all children of element */ button > * /* element and all children */ button, button > * /* all descendants of element */ button * /* element and all descendants */ button, button * ``` ### Class? I'm not sure if implementing something similar to `class` in HTML is necessary. It would be nice to be able to generalize styles though. For completeness, I'll describe the implementation: ```rs // ui ui.add_class("class") // widget (take a generic parameter) ui.add_with_class("class", Button::new("button")); ui.add_with_class(["class1", "class2"], Button::new("button")); // alternate syntax ui.add(Button::new("button").class("class")); ui.add(Button::new("button").class(["class1", "class2"])); ``` ```css /* class selector */ .class ``` ### Implementation Notes I don't think we should allow crazy combinators like `:is` and `:has`, just the basic ones. Even so, basic CSS selectors can get complicated. ```css main:dark > #menu_bar button:hover ``` In addition to being annoying to implement, isn't this kind of overkill for a ui library? We basically have to make our own DOM every frame. If it's relatively straightforward to implement, I think we should to allow the flexibility. If not, we should probably force the selectors to be simple and make this determination for whether the rules apply to the hierarchy some other way . ```css /* just the element */ button /* I personally think the children selector is unecessary -- would also complicate the implementation */ /* all children of element */ button > /* element and all children */ button & > /* all descendants of element */ button * /* element and all descendants */ button &* /* selectors after the hierarchy selector are not allowed */ button * :hover :light /* no multi-part selectors */ button:hover child /* Splitting individual selectors with spaces is not allowed to prevent * * confusion with CSS's descendant combinator */ ``` Also, if a rule is invalid or something, should we just ignore it and emit a warning? Or should the whole style sheet be disallowed to load? ## User-Defined Styles I'm not sure if this was addressed, or intended, in the original proposal, but I think I've figured out a pretty nice way to do custom, user-defined `Style` structs as an alternative to the `Plugin` system. It seems pretty clear that any user-defined `Style` structs, like the ones in the initial proposal, would need to implement some sort of trait to convert from a set of properties: ```rs /// stand-in for the actual structs struct StyleProperties(HashMap); struct FromStylePropertiesErr<'a> { /// properties that were not present on the struct not_found: Box<[&'a str]>, /// properties that threw an error when converting from a string /// FromStr::Err doesn't have any bounds but it should at least implement Display error: Box<[(&'a str, Box)]>, } /// `FromStyleProperties` is just an ugly name and the actual implementation of /// this trait will probably be different, but `Style` just doesn't feel /// descriptive enough. /// /// if anyone has a better name shoot trait Style : Default { fn from_style_properties<'a>(props: &'a StyleProperties) -> (Self, FromStylePropertiesErr<'a>); // ... } ``` With a derive macro, all a user would need to do to define a style struct is: ```rs #[derive(Style)] struct MenuBarStyle { my_color: Color32, // ... } ``` The egui context could then just retrieve the relevant style rules and apply them. ```rs fn menu_bar(ui: &mut Ui) { ui.horizontal_top(|ui| { // now all the styles for the menu bar are defined and used in one place! let style: &MenuBarStyle = ui.id("menu_bar").get_style(); // etc... }); } ``` Naturally, `get_style` would reference the style rules to return the correct styles in this context. I don't think it's possible to cache the whole `CustomStyle` without a completely different API than the one described here. This is probably fine because if properties are cached, it should be relatively cheap to construct. Maybe egui internals like `WidgetStyle` and `TextStyle` could be cached, but also maybe it's such a tiny performance hit it doesn't matter. We can benchmark it and come to conclusions later, but for now I think this API is nice. ## Applying Identifiers ```rs ui.add_with_id("id", Button::new("button")); // alternate syntax ui.add(Button::new("button").id("id")); // alternate syntax impl Widget for Foo { fn ui(ui: &mut Ui) -> Response { ui.style_of("foo"); } } ``` ```rs let style: &Style = ui.set_id("id").get_style(); // more similar to original proposal let style: &Style = ui.style_of("id"); ``` I think the best solution is the latter one iun both cases. It's closer to the original proposal and half-solves another issue... ### ID Ambiguity I'm not completely sure if we should call the identifier used to style, `id`, to avoid confusion with [`Id`], which has the same exact name. If we did keep it as `id`, and used `Ui::set_id`, `Ui::id` and `Ui::set_id` would refer to different `id`'s. `Ui::id` retrieving the `Ui`'s [`Id`], and `Ui::set_id` setting the `Ui`'s style `id`. See how confusing this is? Unfortunately `id` is far and above the best and most obvious name for this. I'm going to use `id` for now, but I am very much open to suggestions. Options for dealing with this are: - Only using `class` and forgoing `id` entirely (`id` is basically just a less flexible `class` anyway) - Calling `id` something else - Not allowing the ui / widget to be given a class or id. `Ui::style_of` takes in a selector. Still a little confusing but much better ## One-Off Styles I think it would be nice to be able to apply a one-off style definition to a `Ui` for reasons hopefully self-apparent. Unfortunately, `Ui::style` and `Ui::set_style` are already taken. I'm honestly not sure what to name this, if implemented. Is `one_off_style` good enough? ```rs // generic parameters my beloved ui.one_off_style("custom.property", "value"); ui.one_off_style("custom.property", Color32::RED); ``` --- oh my god im finally done writing this thing im free its been hours

Summary / Unanswered Questions

"Wow it's almost like a real RFC!"

Questions with recommendations have their recommendations (italicized). Unanswered questions are bolded.

Style Language

User Defined Styles

Applying Identifiers

One-Off Styles

aspiringLich commented 7 months ago

I was going to start working on this right after finishing writing the proposal but now I'm worn out haha

emilk commented 7 months ago

The way I want to approach this is step-by-step:

First implement the new WidgetStyle and use that for all widgets. That already is quite a bit of work, but is mostly refactoring. At this point the WidgetStyle would be selected by something hard-coded in the current struct Style.

Next up would be to implement the WidgetStyle selection it via a plugin system (ThemePlugin). This would require also implementing the first portion of StyleModifiers. This would also be the point we add a cache in front of it to speed up repeated queries for slow plugins.

Next up is designing and implementing a hierarchical "class" system and add that as part of StyleModifiers.

And last is the actual CSS language and engine, which can now be fully a separate crate, and opt-in.

aspiringLich commented 7 months ago

That's understandable. I thought doing the CSS parser, being standalone, was better for me as I'm completely unfamiliar with the project internals.

Do you think it would be reasonable for me to attempt the refactor? Or should it be left up to someone more experienced?

Also, a clarification with the plugin system. Where/How would the plugins be registered? With the top level egui context or in the Widget impl?

emilk commented 6 months ago

An action plan has been added to https://github.com/emilk/egui/issues/3284

rustbasic commented 6 months ago

@emilk

This can be a very large and difficult task that takes a long time. CSS is a complex topic, and there are specialists who focus solely on CSS. It is also possible to implement only simple configurations. (Of course, this is the right approach at first.)

It should be possible to use the egui library without using a theme file. When using the egui library, users should not be forced to use a theme file.