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:
33
Cargo.lock
33
Cargo.lock
@@ -2715,38 +2715,6 @@ version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "lz4_flex"
|
||||
version = "0.13.1"
|
||||
@@ -4520,7 +4488,6 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"env_logger",
|
||||
"logos",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -17,4 +17,3 @@ eframe = { workspace = true, features = [
|
||||
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
|
||||
] }
|
||||
env_logger = { workspace = true, features = ["auto-color", "humantime"] }
|
||||
logos = "0.16.1"
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
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
|
||||
```
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
@@ -1,102 +1,164 @@
|
||||
#![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
|
||||
|
||||
//! 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, Button, Frame, Margin, Panel, UiBuilder,
|
||||
theme::ThemeCache,
|
||||
widget_style::{ButtonStyle, HasClasses as _},
|
||||
self, CentralPanel, Color32, Frame, Panel,
|
||||
theme::StyleProvider,
|
||||
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 {
|
||||
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, 400.0]),
|
||||
viewport: egui::ViewportBuilder::default().with_inner_size([600.0, 340.0]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut style_code = "
|
||||
.red {
|
||||
fill: #f00;
|
||||
}
|
||||
let mut preset = Preset::Ocean;
|
||||
let mut theme = MyTheme::preset(preset);
|
||||
let mut last_click = "nothing";
|
||||
|
||||
.blue {
|
||||
fill: #00f;
|
||||
border: 10;
|
||||
}
|
||||
"
|
||||
.to_owned();
|
||||
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.
|
||||
// Wrap it in a `ThemeCache` if computing the style is expensive.
|
||||
ui.add_widget_theme::<ButtonStyle>(theme);
|
||||
|
||||
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| {
|
||||
// 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.separator();
|
||||
|
||||
ui.scope_builder(UiBuilder::new().with_class("body"), |ui| {
|
||||
ui.label("body");
|
||||
ui.label("central panel");
|
||||
ui.heading("Button theme");
|
||||
|
||||
Panel::left("style_code").show(ui, |ui| {
|
||||
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)",
|
||||
);
|
||||
let mut changed = false;
|
||||
|
||||
if ui.text_edit_multiline(&mut style_code).changed()
|
||||
&& let Ok(engine) = ESSEngine::try_parse(&style_code)
|
||||
{
|
||||
// Overwrite the current theme with the new one.clear
|
||||
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")
|
||||
));
|
||||
})
|
||||
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.add(Button::new("Normal"));
|
||||
ui.add(Button::new("red").with_class("red"));
|
||||
ui.add(Button::new("blue").with_class("blue"));
|
||||
ui.add(Button::new("dynamic in engine A").with_class("dynamic"));
|
||||
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(
|
||||
Button::new("red/blue")
|
||||
.with_class_if("red", toggled)
|
||||
.with_class_if("blue", !toggled),
|
||||
)
|
||||
.add(egui::Button::new("Delete everything").with_class(DANGER))
|
||||
.clicked()
|
||||
{
|
||||
toggled = !toggled;
|
||||
last_click = "Delete everything";
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.label(format!("Last clicked: {last_click}"));
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
.red {
|
||||
fill: #f00;
|
||||
}
|
||||
|
||||
.blue {
|
||||
fill: #00f;
|
||||
border: 4;
|
||||
}
|
||||
Reference in New Issue
Block a user