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
- Do all widget interaction at the start of the frame #3936
- CSS-like shadows with offset, spread, and blur #4232
- CSS-like
Border
#4019 - Use
Frame
for most widgets (all but label?) - Implement the new
WidgetStyle
and use that for all widgets, with first iteration ofStyleModifiers
- Implement the
WidgetStyle
selection it via a plugin system (ThemePlugin
). - Cache the output of the
ThemePlugin
- Implement some hierarchical "class" system and add that as part of
StyleModifiers
- The actual CSS rule engine, which can now be fully a separate crate, and opt-in
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
Labels
Projects
Status
No status