Skip to content

CSS-like styling #3284

Open
Open
@emilk

Description

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?

Metadata

Assignees

No one assigned

    Labels

    rerunDesired for Rerun.io

    Projects

    • Status

      No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions