Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CSS-like styling #3284

Open
2 of 8 tasks
emilk opened this issue Aug 28, 2023 · 11 comments
Open
2 of 8 tasks

CSS-like styling #3284

emilk opened this issue Aug 28, 2023 · 11 comments
Labels
rerun Desired for Rerun.io

Comments

@emilk
Copy link
Owner

emilk commented Aug 28, 2023

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:

  • widget type (button, slider, label, …)
  • interact state (disabled, inactive, active, hovered, active)
  • text modifier (header, small, weak, strong, code, …)
  • per-Ui identifier (”settings_panel”)
  • per-widget identifier (”exit_button”)

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 #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:

  • How do we distinguish between different modifier types? Do we need to?
  • How do we specify if a rule applies to:
    • The widget
    • The widget and all children
    • Just the children

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
Copy link
Collaborator

abey79 commented Aug 29, 2023

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
Copy link

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
Copy link

jmetz commented Oct 25, 2023

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
Copy link

chris-kruining commented Oct 25, 2023

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
Copy link

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

@emilk emilk added the rerun Desired for Rerun.io label Nov 17, 2023
@emilk emilk unpinned this issue Dec 28, 2023
@emilk emilk mentioned this issue Dec 28, 2023
@aspiringLich
Copy link

aspiringLich commented Jan 4, 2024

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

// 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
}
/* 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:

/* 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:

// 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"]));
/* 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.

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 .

/* 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:

/// stand-in for the actual structs
struct StyleProperties(HashMap<String, String>);

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<dyn Display>)]>,
}

/// `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:

#[derive(Style)]
struct MenuBarStyle {
    my_color: Color32,
    // ...
}

The egui context could then just retrieve the relevant style rules and apply them.

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

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");
    }
}
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?

// 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

  • Should we make the style language more like CSS? (yes)
    • If so, do we want CSS-like combinators?
      • Which of the hierarchical combinators are feasible to implement? ( , >, +, ~)
    • If not, what would our own solution be ? (described in proposal)
    • Should we permit errors in stylesheets or reject them entirely? (permit)
  • class?

User Defined Styles

  • Should we allow user-defined styles? (decided this was a bad idea)

Applying Identifiers

  • How should we assign Uis and Widgets with identifiers?
  • Should we rename style id to prevent confusion with Id?

One-Off Styles

  • Should we allow one-off style definitions?
    • What should the API be?

@aspiringLich
Copy link

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

@emilk
Copy link
Owner Author

emilk commented Jan 4, 2024

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
Copy link

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
Copy link
Owner Author

emilk commented Feb 10, 2024

An action plan has been added to #3284

@rustbasic
Copy link
Contributor

@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.

@abey79 abey79 mentioned this issue May 31, 2024
2 tasks
abey79 added a commit that referenced this issue Jun 4, 2024
* Closes #4534

This PR:
- Introduces `Ui::stack()`, which returns the `UiStack` structure
providing information on the current `Ui` hierarchy.
- **BREAKING**: `Ui::new()` now takes a `UiStackInfo` argument, which is
used to populate some of this `Ui`'s `UiStack`'s fields.
- **BREAKING**: `Ui::child_ui()` and `Ui::child_ui_with_id_source()` now
take an `Option<UiStackInfo>` argument, which is used to populate some
of the children `Ui`'s `UiStack`'s fields.
- New `Area::kind()` builder function, to set the `UiStackKind` value of
the `Area`'s `Ui`.
- Adds a (minimalistic) demo to egui demo (in the "Misc Demos" window).
- Adds a more thorough `test_ui_stack` test/playground demo.

TODO:
- [x] benchmarks
- [x] add example to demo

Future work:
- Add `UiStackKind` and related support for more container (e.g.
`CollapsingHeader`, etc.)
- Add a tag/property system that would allow adding arbitrary data to a
stack node. This data could then be queried by nested `Ui`s. Probably
needed for #3284.
- Add support to track columnar layouts.

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
@juancampa juancampa mentioned this issue Aug 11, 2024
1 task
emilk pushed a commit that referenced this issue Aug 30, 2024
<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md)
before opening a Pull Request!

* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to test and add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.

Please be patient! I will review your PR, but my time is limited!
-->


* Closes <#4776>
* [x] I have followed the instructions in the PR template



I've been meaning to look into this for a while but finally bit the
bullet this week. Contrary to what I initially thought, the problem of
blurry lines is unrelated to feathering because it also happens with
feathering disabled.

The root cause is that lines tend to land on pixel boundaries, and
because of that, frequently used strokes (e.g. 1pt), end up partially
covering pixels. This is especially noticeable on 1ppp displays.

There were a couple of things to fix, namely: individual lines like
separators and indents but also shape strokes (e.g. Frame).

Lines were easy, I just made sure we round them to the nearest pixel
_center_, instead of the nearest pixel boundary.

Strokes were a little more complicated. To illustrate why, here’s an
example: if we're rendering a 5x5 rect (black fill, red stroke), we
would expect to see something like this:

![Screenshot 2024-08-11 at 15 01
41](https://github.com/user-attachments/assets/5a5d4434-0814-451b-8179-2864dc73c6a6)

The fill and the stroke to cover entire pixels. Instead, egui was
painting the stroke partially inside and partially outside, centered
around the shape’s path (blue line):

![Screenshot 2024-08-11 at 15 00
57](https://github.com/user-attachments/assets/4284dc91-5b6e-4422-994a-17d527a6f13b)

Both methods are valid for different use-cases but the first one is what
we’d typically want for UIs to feel crisp and pixel perfect. It's also
how CSS borders work (related to #4019 and #3284).

Luckily, we can use the normal computed for each `PathPoint` to adjust
the location of the stroke to be outside, inside, or in the middle.
These also are the 3 types of strokes available in tools like Photoshop.

This PR introduces an enum `StrokeKind` which determines if a
`PathStroke` should be tessellated outside, inside, or _on_ the path
itself. Where "outside" is defined by the directions normals point to.

Tessellator will now use `StrokeKind::Outside` for closed shapes like
rect, ellipse, etc. And `StrokeKind::Middle` for the rest since there's
no meaningful "outside" concept for open paths. This PR doesn't expose
`StrokeKind` to user-land, but we can implement that later so that users
can render shapes and decide where to place the stroke.

### Strokes test
(blue lines represent the size of the rect being rendered)

`Stroke::Middle` (current behavior, 1px and 3px are blurry)
![Screenshot 2024-08-09 at 23 55
48](https://github.com/user-attachments/assets/dabeaa9e-2010-4eb6-bd7e-b9cb3660542e)


`Stroke::Outside` (proposed default behavior for closed paths)
![Screenshot 2024-08-09 at 23 51
55](https://github.com/user-attachments/assets/509c261f-0ae1-46a0-b9b8-08de31c3bd85)



`Stroke::Inside` (for completeness but unused at the moment)
![Screenshot 2024-08-09 at 23 54
49](https://github.com/user-attachments/assets/c011b1c1-60ab-4577-baa9-14c36267438a)



### Demo App
The best way to review this PR is to run the demo on a 1ppp display,
especially to test hover effects. Everything should look crisper. Also
run it in a higher dpi screen to test that nothing broke 🙏.

Before:

![egui_old](https://github.com/user-attachments/assets/cd6e9032-d44f-4cb0-bb41-f9eb4c3ae810)


After (notice the sharper lines):

![egui_new](https://github.com/user-attachments/assets/3365fc96-6eb2-4e7d-a2f5-b4712625a702)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
rerun Desired for Rerun.io
Projects
None yet
Development

No branches or pull requests

7 participants