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

A much simpler example

This commit is contained in:
Emil Ernerfeldt
2026-08-04 14:03:05 +02:00
parent 3a584a620e
commit 37eb350124
6 changed files with 139 additions and 249 deletions

View File

@@ -2715,38 +2715,6 @@ version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "logos"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb2c55a318a87600ea870ff8c2012148b44bf18b74fad48d0f835c38c7d07c5f"
dependencies = [
"logos-derive",
]
[[package]]
name = "logos-codegen"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58b3ffaa284e1350d017a57d04ada118c4583cf260c8fb01e0fe28a2e9cf8970"
dependencies = [
"fnv",
"proc-macro2",
"quote",
"regex-automata",
"regex-syntax",
"syn",
]
[[package]]
name = "logos-derive"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52d3a9855747c17eaf4383823f135220716ab49bea5fbea7dd42cc9a92f8aa31"
dependencies = [
"logos-codegen",
]
[[package]] [[package]]
name = "lz4_flex" name = "lz4_flex"
version = "0.13.1" version = "0.13.1"
@@ -4520,7 +4488,6 @@ version = "0.1.0"
dependencies = [ dependencies = [
"eframe", "eframe",
"env_logger", "env_logger",
"logos",
] ]
[[package]] [[package]]

View File

@@ -12,9 +12,8 @@ workspace = true
[dependencies] [dependencies]
eframe = { workspace = true, features = [ eframe = { workspace = true, default-features = false, features = [
"default", "glow", "default_fonts",
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
] } ] }
env_logger = { workspace = true, features = ["auto-color", "humantime"] } env_logger = { workspace = true, features = ["auto-color", "humantime"] }
logos = "0.16.1"

View File

@@ -1,5 +1,8 @@
Example showing how the style engine work. 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 ```sh
cargo run -p styling_engine cargo run -p styling_engine
``` ```

View File

@@ -1,133 +0,0 @@
use std::collections::HashMap;
use eframe::egui::{
Color32,
theme::StyleProvider,
widget_style::{BaseStyle, ButtonStyle, HasClasses as _, StyleArgs},
};
use logos::Logos;
#[derive(Debug, Default, Clone)]
pub struct ESSEngine {
info: HashMap<String, Vec<(String, Value)>>,
}
impl ESSEngine {
pub fn try_parse(ess: &str) -> Result<Self, String> {
let mut engine = Self::default();
let mut lexer = Token::lexer(ess);
let mut hash = HashMap::new();
while let Some(token) = lexer.next() {
if token == Ok(Token::Class) {
let selector = lexer.slice()[1..].to_owned();
if lexer
.next()
.is_some_and(|token| token.is_ok_and(|token| token != Token::Open))
{
return Err("No opening bracket found !".to_owned());
}
let mut declarations = vec![];
loop {
match lexer.next() {
Some(Ok(Token::Property)) => {
let property = lexer.slice().to_owned();
if lexer
.next()
.is_some_and(|token| token.is_ok_and(|token| token != Token::Is))
{
return Err("No separator between property and value !".to_owned());
}
let value = match lexer.next() {
Some(Ok(Token::Number)) => Value::Number(
lexer
.slice()
.to_owned()
.parse::<usize>()
.expect("Should be a positive integer"),
),
Some(Ok(Token::Color)) => Value::Color(
Color32::from_hex(lexer.slice())
.expect("Should be a valid hex"),
),
Some(Ok(v)) => return Err(format!("Invalid value : {v:?}")),
_ => return Err("Error".to_owned()),
};
declarations.push((property, value));
}
Some(Ok(Token::Close)) => break,
Some(Ok(v)) => {
return Err(format!("Missing close bracket, found : {v:?}"));
}
v => return Err(format!("Error : {v:?}")),
}
}
hash.insert(selector, declarations);
}
}
engine.info = hash;
Ok(engine)
}
}
impl StyleProvider<ButtonStyle> for ESSEngine {
fn style(&mut self, modifiers: &StyleArgs<'_>) -> ButtonStyle {
let StyleArgs { classes, ctx, .. } = modifiers;
let base = ctx.get_widget_style::<BaseStyle>(modifiers);
let mut default = ButtonStyle {
frame: base.frame,
text_style: base.text,
};
for class in classes.as_slice() {
if let Some(properties) = self.info.get(&class.to_string()) {
for (property, value) in properties {
match property.as_str() {
"fill" => {
if let Value::Color(color) = value {
default.frame.fill = *color;
}
}
"border" => {
if let Value::Number(size) = value {
default.frame.stroke.width = *size as f32;
}
}
_ => {}
}
}
}
}
default
}
}
#[derive(Debug, Logos, PartialEq)]
enum Token {
#[token("{")]
Open,
#[token("}")]
Close,
#[token(":")]
Is,
#[regex(r"\.[a-zA-Z]+")]
Class,
#[regex(r"[a-zA-Z]+")]
Property,
#[regex(r"[0-9]+")]
Number,
#[regex(r"#(?:[0-9a-fA-F]{3}){1,2}")]
Color,
#[regex(r"[ \t\n\f;]+", logos::skip)]
Whitespace,
}
#[derive(Debug, Clone)]
enum Value {
Number(usize),
Color(Color32),
}

View File

@@ -1,102 +1,164 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
#![expect(rustdoc::missing_crate_level_docs)] // it's an example #![expect(rustdoc::missing_crate_level_docs)] // it's an example
//! 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::{ use eframe::egui::{
self, Button, Frame, Margin, Panel, UiBuilder, self, CentralPanel, Color32, Frame, Panel,
theme::ThemeCache, theme::StyleProvider,
widget_style::{ButtonStyle, HasClasses as _}, widget_style::{BaseStyle, ButtonStyle, HasClasses as _, StyleArgs, WidgetState},
}; };
use crate::custom_engine::ESSEngine; /// Buttons with this class are styled as a destructive action.
const DANGER: &str = "danger";
mod custom_engine; /// 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 { fn main() -> eframe::Result {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let options = eframe::NativeOptions { let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([600.0, 400.0]), viewport: egui::ViewportBuilder::default().with_inner_size([600.0, 340.0]),
..Default::default() ..Default::default()
}; };
let mut style_code = " let mut preset = Preset::Ocean;
.red { let mut theme = MyTheme::preset(preset);
fill: #f00; let mut last_click = "nothing";
}
.blue { eframe::run_ui_native("Styling engine", options, move |ui, _frame| {
fill: #00f; // Register our theme for all buttons. This is a no-op after the first frame.
border: 10; // Wrap it in a `ThemeCache` if computing the style is expensive.
} ui.add_widget_theme::<ButtonStyle>(theme);
"
.to_owned();
let mut toggled = false; 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);
eframe::run_ui_native("My egui App", options, move |ui, _frame| { ui.separator();
// Register the theme plugin and which style they implement
if let Ok(engine) = ESSEngine::try_parse(&style_code) {
ui.add_widget_theme::<ButtonStyle>(ThemeCache::new(engine));
}
ui.scope_builder(UiBuilder::new().with_class("body"), |ui| { ui.heading("Button theme");
ui.label("body");
ui.label("central panel");
Panel::left("style_code").show(ui, |ui| { let mut changed = false;
ui.scope_builder(UiBuilder::new().with_class("panel_left"), |ui| {
ui.label(
"Live editor\n(type color hex to change the color of the dynamic button)",
);
if ui.text_edit_multiline(&mut style_code).changed() egui::ComboBox::from_label("Preset")
&& let Ok(engine) = ESSEngine::try_parse(&style_code) .selected_text(match preset {
{ Preset::Ocean => "Ocean",
// Overwrite the current theme with the new one.clear Preset::Candy => "Candy",
ui.replace_widget_theme::<ButtonStyle>(engine);
}
});
});
ui.scope_builder(UiBuilder::new().with_class("grid"), |ui| {
Frame::new().inner_margin(Margin::same(10)).show(ui, |ui| {
ui.scope_builder(UiBuilder::new().with_class("frame1"), |ui| {
let mut parent = Some(ui.stack());
let mut text = vec![];
let mut i: i32 = 0;
while let Some(p) = parent {
text.push(format!(
"{}{}class : '{}', kind : {:?}",
" ".repeat((2 * 0_i32.max(i - 1) + 1.min(i)) as usize),
if i > 0 { "\\- " } else { "" },
p.classes,
p.kind()
));
i += 1;
parent = p.parent.as_ref();
}
ui.label(format!(
"Current hierarchy (child to root):\n{}",
text.join("\n")
));
})
}) })
.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);
}
}); });
ui.add(Button::new("Normal")); CentralPanel::default().show(ui, |ui| {
ui.add(Button::new("red").with_class("red")); ui.heading("Buttons");
ui.add(Button::new("blue").with_class("blue")); ui.label("All buttons below are styled by the custom theme.");
ui.add(Button::new("dynamic in engine A").with_class("dynamic")); ui.add_space(8.0);
if ui.button("Save").clicked() {
last_click = "Save";
}
ui.add_space(4.0);
if ui if ui
.add( .add(egui::Button::new("Delete everything").with_class(DANGER))
Button::new("red/blue")
.with_class_if("red", toggled)
.with_class_if("blue", !toggled),
)
.clicked() .clicked()
{ {
toggled = !toggled; last_click = "Delete everything";
} }
ui.add_space(8.0);
ui.label(format!("Last clicked: {last_click}"));
}); });
}) })
} }

View File

@@ -1,8 +0,0 @@
.red {
fill: #f00;
}
.blue {
fill: #00f;
border: 4;
}