1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-29 04:40:03 -04:00

Theme plugin system (experimental) (#8153)

<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/main/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!
-->

# What it does

Addition of a new system of theme plugins which allow the user to use
different rules engine to compute the style for the available
specialised widget style.

# How to use

Create a engine implementing the trait `ThemePlugin` and `ThemeStyle<S:
StyleStruct>` and implement the necessary methods, then register this
way (example for `ButtonStyle`):
```ui.add_theme::<ButtonStyle>(&mycustomengine);```

Now all button will call the `ThemeStyle<ButtonStyle>` method to compute the correct style and later use the cached value to avoid the costly computation.

If no valid `ThemeStyle<S>` or engine is available then it fallback to the default style.

* Closes part of <https://github.com/emilk/egui/issues/3284>
* [x] I have followed the instructions in the PR template

---------

Co-authored-by: adrien <221212@umons.ac.be>
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Lucas Meurer <hi@lucasmerlin.me>
This commit is contained in:
YouStones
2026-08-21 15:05:45 +02:00
committed by GitHub
parent f5c9373e26
commit 97603fc082
21 changed files with 857 additions and 336 deletions

View File

@@ -0,0 +1,20 @@
[package]
name = "styling_engine"
version = "0.1.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2024"
rust-version = "1.92"
publish = false
[lints]
workspace = true
[dependencies]
eframe = { workspace = true, features = [
"default",
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
"experimental", # this example is all about the experimental theming API
] }
env_logger = { workspace = true, features = ["auto-color", "humantime"] }

View File

@@ -0,0 +1,10 @@
Example showing how the style engine work.
A custom `StyleProvider<ButtonStyle>` styles every button, using _classes_ to tell them apart.
The theme can be edited live in the left panel.
```sh
cargo run -p styling_engine
```
<!-- ![](screenshot.png) -->

View File

@@ -0,0 +1,162 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
//! A small styling engine: a custom [`StyleProvider`] that styles every [`egui::Button`]
//! based on the _classes_ set on it, and that can be edited live.
use eframe::egui::{
self, CentralPanel, Color32, Frame, Panel,
theme::StyleProvider,
widget_style::{BaseStyle, ButtonStyle, HasClasses as _, StyleArgs, WidgetState},
};
/// Buttons with this class are styled as a destructive action.
const DANGER: &str = "danger";
/// Our styling engine: it decides what every button looks like.
#[derive(Clone, Copy, PartialEq)]
struct MyTheme {
normal: Color32,
danger: Color32,
corner_radius: u8,
}
impl MyTheme {
fn preset(preset: Preset) -> Self {
match preset {
Preset::Ocean => Self {
normal: Color32::from_rgb(0x1E, 0x5A, 0x8A),
danger: Color32::from_rgb(0x9B, 0x2C, 0x2C),
corner_radius: 4,
},
Preset::Candy => Self {
normal: Color32::from_rgb(0xB8, 0x3B, 0x9E),
danger: Color32::from_rgb(0xD9, 0x6A, 0x1F),
corner_radius: 16,
},
}
}
}
impl StyleProvider<ButtonStyle> for MyTheme {
fn style(&mut self, args: &StyleArgs<'_>) -> ButtonStyle {
let StyleArgs {
classes,
state,
ctx,
..
} = args;
// Start from the style egui computed for a generic widget, so we inherit e.g. the font:
let base: BaseStyle = ctx.get_widget_style(args);
let fill = if classes.has(DANGER) {
self.danger
} else {
self.normal
};
// React to what the user is doing with the button:
let fill = match state {
WidgetState::Hovered => fill.gamma_multiply(1.4),
WidgetState::Active => fill.gamma_multiply(0.7),
_ => fill,
};
ButtonStyle {
frame: Frame::new()
.fill(fill)
.corner_radius(self.corner_radius)
.inner_margin(8),
text_style: base.text,
}
}
}
#[derive(Clone, Copy, PartialEq)]
enum Preset {
Ocean,
Candy,
}
fn main() -> eframe::Result {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([600.0, 340.0]),
..Default::default()
};
let mut preset = Preset::Ocean;
let mut theme = MyTheme::preset(preset);
let mut last_click = "nothing";
eframe::run_ui_native("Styling engine", options, move |ui, _frame| {
// Register our theme for all buttons. This is a no-op after the first frame.
ui.add_widget_theme::<ButtonStyle>(theme);
Panel::left("controls").default_size(260.0).show(ui, |ui| {
// The custom theme inherits the text color from egui's light/dark theme:
egui::global_theme_preference_buttons(ui);
ui.separator();
ui.heading("Button theme");
let mut changed = false;
egui::ComboBox::from_label("Preset")
.selected_text(match preset {
Preset::Ocean => "Ocean",
Preset::Candy => "Candy",
})
.show_ui(ui, |ui| {
changed |= ui
.selectable_value(&mut preset, Preset::Ocean, "Ocean")
.changed();
changed |= ui
.selectable_value(&mut preset, Preset::Candy, "Candy")
.changed();
});
if changed {
theme = MyTheme::preset(preset);
}
ui.horizontal(|ui| {
changed |= ui.color_edit_button_srgba(&mut theme.normal).changed();
ui.label("Normal");
});
ui.horizontal(|ui| {
changed |= ui.color_edit_button_srgba(&mut theme.danger).changed();
ui.label("Danger");
});
changed |= ui
.add(egui::Slider::new(&mut theme.corner_radius, 0..=24).text("Corner radius"))
.changed();
if changed {
// Overwrite the registered theme with the edited one:
ui.replace_widget_theme::<ButtonStyle>(theme);
}
});
CentralPanel::default().show(ui, |ui| {
ui.heading("Buttons");
ui.label("All buttons below are styled by the custom theme.");
ui.add_space(8.0);
if ui.button("Save").clicked() {
last_click = "Save";
}
ui.add_space(4.0);
if ui
.add(egui::Button::new("Delete everything").with_class(DANGER))
.clicked()
{
last_click = "Delete everything";
}
ui.add_space(8.0);
ui.label(format!("Last clicked: {last_click}"));
});
})
}