mirror of
https://github.com/emilk/egui.git
synced 2026-09-02 23:00:04 -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"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"eframe",
|
"eframe",
|
||||||
|
"egui",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -169,7 +169,12 @@ pub struct WidgetStyle {
|
|||||||
/// Cons:
|
/// Cons:
|
||||||
/// - Style changes across all widgets would require more code changes.
|
/// - 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?
|
/// - 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.
|
/// - 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 struct CheckboxStyle {
|
||||||
pub text: TextFormat,
|
pub text: TextFormat,
|
||||||
@@ -180,7 +185,15 @@ pub struct CheckboxStyle {
|
|||||||
custom_checkmark_painter: Option<Box<dyn Fn(&Painter, Rect)>>,
|
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 {
|
impl From<WidgetStyle> for WidgetVisuals {
|
||||||
fn from(value: WidgetStyle) -> Self {
|
fn from(value: WidgetStyle) -> Self {
|
||||||
|
|||||||
@@ -14,9 +14,14 @@ workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
eframe = { workspace = true, features = [
|
eframe = { workspace = true, features = [
|
||||||
"default",
|
"default",
|
||||||
|
"serde",
|
||||||
"__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 = { version = "0.10", default-features = false, features = [
|
env_logger = { version = "0.10", default-features = false, features = [
|
||||||
"auto-color",
|
"auto-color",
|
||||||
"humantime",
|
"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
|
#![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
|
#![allow(rustdoc::missing_crate_level_docs)] // it's an example
|
||||||
|
|
||||||
|
mod ess_style_engine;
|
||||||
|
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use eframe::egui::style::WidgetVisuals;
|
use eframe::egui::style::WidgetVisuals;
|
||||||
use eframe::egui::style_trait::{
|
use eframe::egui::style_trait::{
|
||||||
@@ -11,6 +13,8 @@ use eframe::egui::{
|
|||||||
};
|
};
|
||||||
use eframe::emath::TSTransform;
|
use eframe::emath::TSTransform;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use crate::ess_style_engine::{EssFile, EssStyleEngine};
|
||||||
|
|
||||||
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`).
|
||||||
@@ -24,10 +28,18 @@ fn main() -> eframe::Result {
|
|||||||
let mut name = "Arthur".to_owned();
|
let mut name = "Arthur".to_owned();
|
||||||
let mut age = 42;
|
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| {
|
eframe::run_simple_native("My egui App", options, move |ctx, _frame| {
|
||||||
ctx.set_style_engine(MyCustomWidgetStyle {
|
if let Some(custom_engine) = custom_engine.take() {
|
||||||
default: DefaultWidgetStyle,
|
// ctx.set_style_engine(custom_engine);
|
||||||
});
|
|
||||||
|
ctx.set_style_engine(EssStyleEngine::new(custom_engine, styles.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
ui.heading("My egui Application");
|
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("Large Secondary").secondary().lg());
|
||||||
ui.add(Button::new("Small Normal").sm());
|
ui.add(Button::new("Small Normal").sm());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ui.add(Button::new("Customized via ESS").with_class("blue"));
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user