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"
|
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]]
|
||||||
|
|||||||
@@ -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"
|
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
#![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.add(Button::new("Normal"));
|
ui.horizontal(|ui| {
|
||||||
ui.add(Button::new("red").with_class("red"));
|
changed |= ui.color_edit_button_srgba(&mut theme.normal).changed();
|
||||||
ui.add(Button::new("blue").with_class("blue"));
|
ui.label("Normal");
|
||||||
ui.add(Button::new("dynamic in engine A").with_class("dynamic"));
|
});
|
||||||
|
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
|
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}"));
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
.red {
|
|
||||||
fill: #f00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blue {
|
|
||||||
fill: #00f;
|
|
||||||
border: 4;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user