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

Add draft of a css-like style rule engine

This commit is contained in:
lucasmerlin
2025-08-07 18:24:07 +02:00
parent 36a4324996
commit 85cbe93214
6 changed files with 297 additions and 4 deletions

View File

@@ -2069,7 +2069,10 @@ name = "hello_world_simple"
version = "0.1.0"
dependencies = [
"eframe",
"egui",
"env_logger",
"serde",
"serde_json",
]
[[package]]

View File

@@ -169,7 +169,12 @@ pub struct WidgetStyle {
/// Cons:
/// - Style changes across all widgets would require more code changes.
/// - Maybe there could be a shared base WidgetStyle though that defines things like strokes and base colors?
/// - Or these things could keep coming from the `Style` struct
/// - More boilerplate code for each widget.
/// - Might make it harder to create a css-like file format since it would need support all the custom types
/// - Maybe could be possible with serde or a extra trait that allows for some kind of reflection?
///
/// ```
///
pub struct CheckboxStyle {
pub text: TextFormat,
@@ -180,7 +185,15 @@ pub struct CheckboxStyle {
custom_checkmark_painter: Option<Box<dyn Fn(&Painter, Rect)>>,
}
pub struct ButtonStyle {}
pub struct ButtonStyle {
/// Background color, stroke, margin, and shadow.
pub frame: Frame,
/// What font to use and at what size.
pub text: TextFormat,
pub transform: TSTransform,
}
impl From<WidgetStyle> for WidgetVisuals {
fn from(value: WidgetStyle) -> Self {

View File

@@ -14,9 +14,14 @@ workspace = true
[dependencies]
eframe = { workspace = true, features = [
"default",
"serde",
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
] }
env_logger = { version = "0.10", default-features = false, features = [
"auto-color",
"humantime",
] }
egui = { workspace = true, features = ["serde"] }
serde_json = "1"
serde = "1"

View File

@@ -0,0 +1,236 @@
use eframe::egui;
use eframe::egui::Frame;
use eframe::egui::style_trait::{ButtonStyle, StyleEngine, WidgetContext, WidgetStyle};
use std::any::TypeId;
use std::collections::HashMap;
use std::sync::Arc;
pub trait Stylable {
fn name() -> &'static str;
fn set_property(&mut self, key: &[&str], value: &str) -> Result<(), StyleError>;
}
type StyleError = Box<dyn std::error::Error + Send + Sync>;
impl Stylable for Frame {
fn name() -> &'static str {
"Frame"
}
fn set_property(&mut self, key: &[&str], value: &str) -> Result<(), StyleError> {
let key = *key.first().ok_or_else(|| "Missing key")?;
match key {
"fill" => {
// Maybe these could also use FromStr?
self.fill = serde_json::from_str(value)?;
}
"stroke" => {
self.stroke = serde_json::from_str(value)?;
}
"corner_radius" => {
self.corner_radius = serde_json::from_str(value)?;
}
"inner_margin" => {
self.inner_margin = serde_json::from_str(value)?;
}
"outer_margin" => {
self.outer_margin = serde_json::from_str(value)?;
}
_ => {
return Err(Box::from(format!("Unknown property: {}", key)));
}
}
Ok(())
}
}
impl Stylable for ButtonStyle {
fn name() -> &'static str {
"Button"
}
fn set_property(&mut self, key: &[&str], value: &str) -> Result<(), StyleError> {
match key {
["frame_fill"] => {
self.frame.fill = serde_json::from_str(value)?;
}
["frame", param] => {
self.frame.set_property(&[param], value)?;
}
// Etc...
_ => {
Err(format!("Unknown property: {:?}", key))?;
}
}
Ok(())
}
}
impl Stylable for WidgetStyle {
fn name() -> &'static str {
"WidgetStyle"
}
fn set_property(&mut self, key: &[&str], value: &str) -> Result<(), StyleError> {
match key {
["frame_fill"] => {
self.frame.fill = serde_json::from_str(value)?;
}
["color"] => {
self.stroke.color = serde_json::from_str(value)?;
self.text.color = serde_json::from_str(value)?; // 😬
}
["frame", param] => {
self.frame.set_property(&[param], value)?;
}
// Etc...
_ => {
Err(format!("Unknown property: {:?}", key))?;
}
}
Ok(())
}
}
pub struct EssStyleEngine<E, T> {
style: Arc<EssFile>,
wrapped_engine: E,
_stylable: std::marker::PhantomData<T>,
}
impl<E: StyleEngine<T>, T: Stylable> EssStyleEngine<E, T> {
pub fn new(engine: E, style: Arc<EssFile>) -> Self {
Self {
style,
wrapped_engine: engine,
_stylable: std::marker::PhantomData,
}
}
}
impl<E: StyleEngine<T>, T: Stylable + Sync + Send> StyleEngine<T> for EssStyleEngine<E, T> {
fn get(&self, ctx: &WidgetContext) -> T {
let name = T::name();
// Ideally there would be some caching here since the set_property calls can be expensive
// (since they do serde deserialization)
let mut style = self.wrapped_engine.get(ctx);
dbg!(name);
if let Some(rules) = self.style.style.get(name) {
let rules = self.style.style.get(name).unwrap();
// This is very primitive, you probably want something like css specificity
for rule in rules {
if rule.check(&ctx) {
for (keys, value) in &rule.properties {
dbg!(keys.as_slice(), value);
style
.set_property(keys.as_slice(), value)
.expect("Failed to set property"); // TODO: Error handling
}
}
}
}
style
}
}
enum State {
Active,
Hovered,
Focused,
Disabled,
}
enum Modifier {
Class(String),
State(State),
}
struct EssRule {
modifiers: Vec<Modifier>,
properties: Vec<(Vec<&'static str>, String)>,
}
impl EssRule {
fn check(&self, ctx: &WidgetContext) -> bool {
// Check if the rule matches the context
for modifier in &self.modifiers {
match modifier {
Modifier::Class(class) => {
if !ctx.classes.has(class) {
return false;
}
}
Modifier::State(state) => match state {
State::Active => {
if !ctx.response.is_pointer_button_down_on() {
return false;
}
}
State::Hovered => {
if !ctx.response.hovered() {
return false;
}
}
State::Focused => {
if !ctx.response.has_focus() {
return false;
}
}
State::Disabled => {
if ctx.response.enabled() {
return false;
}
}
},
}
}
true
}
}
pub struct EssFile {
// Rules for different widget types
style: HashMap<String, Vec<EssRule>>,
}
impl EssFile {
pub fn parse(file: &str) {
todo!()
}
pub fn example() -> Self {
let mut style = HashMap::new();
// Example rule for Button
style.insert(
"WidgetStyle".to_string(),
vec![
EssRule {
modifiers: vec![Modifier::Class("blue".to_string())],
properties: vec![
(vec!["frame_fill"], "[0, 0, 200, 255]".to_string()),
(vec!["color"], "[255, 255, 255, 255]".to_string()),
],
},
EssRule {
modifiers: vec![
Modifier::State(State::Hovered),
Modifier::Class("blue".to_string()),
],
// Serde implementation of color32 could be improved...
properties: vec![
(vec!["frame_fill"], "[0, 0, 255, 255]".to_string()),
],
},
],
);
Self { style }
}
}

View File

@@ -0,0 +1,22 @@
// Should style definitions always be scoped to a widget (or rather a stylable type) for type safety?
Button .blue {
:hovered {
background_color: "#0000ff",
// This should error?
font_soze: 20,
}
color: "#ffffff",
background_color: "#0000aa",
}
.list_item {
Button {
font_size: 10,
}
// Maybe Image could also be stylable so we could set icon sizes based on context?
Image {
width: 12,
height: 12,
}
}

View File

@@ -1,6 +1,8 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
#![allow(rustdoc::missing_crate_level_docs)] // it's an example
mod ess_style_engine;
use eframe::egui;
use eframe::egui::style::WidgetVisuals;
use eframe::egui::style_trait::{
@@ -11,6 +13,8 @@ use eframe::egui::{
};
use eframe::emath::TSTransform;
use std::fmt::Display;
use std::sync::Arc;
use crate::ess_style_engine::{EssFile, EssStyleEngine};
fn main() -> eframe::Result {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
@@ -24,10 +28,18 @@ fn main() -> eframe::Result {
let mut name = "Arthur".to_owned();
let mut age = 42;
let styles = Arc::new(EssFile::example());
let mut custom_engine = Some(MyCustomWidgetStyle {
default: DefaultWidgetStyle,
});
eframe::run_simple_native("My egui App", options, move |ctx, _frame| {
ctx.set_style_engine(MyCustomWidgetStyle {
default: DefaultWidgetStyle,
});
if let Some(custom_engine) = custom_engine.take() {
// ctx.set_style_engine(custom_engine);
ctx.set_style_engine(EssStyleEngine::new(custom_engine, styles.clone()));
}
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("My egui Application");
@@ -53,6 +65,8 @@ fn main() -> eframe::Result {
ui.add(Button::new("Large Secondary").secondary().lg());
ui.add(Button::new("Small Normal").sm());
});
ui.add(Button::new("Customized via ESS").with_class("blue"));
});
})
}