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:
@@ -2069,7 +2069,10 @@ name = "hello_world_simple"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
"env_logger",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
236
examples/hello_world_simple/src/ess_style_engine.rs
Normal file
236
examples/hello_world_simple/src/ess_style_engine.rs
Normal 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 }
|
||||
}
|
||||
}
|
||||
22
examples/hello_world_simple/src/example_style.ess
Normal file
22
examples/hello_world_simple/src/example_style.ess
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user