diff --git a/Cargo.lock b/Cargo.lock index b1ed18559..51db4b32e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2069,7 +2069,10 @@ name = "hello_world_simple" version = "0.1.0" dependencies = [ "eframe", + "egui", "env_logger", + "serde", + "serde_json", ] [[package]] diff --git a/crates/egui/src/style_trait.rs b/crates/egui/src/style_trait.rs index a42d1ff17..46cbd0c63 100644 --- a/crates/egui/src/style_trait.rs +++ b/crates/egui/src/style_trait.rs @@ -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>, } -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 for WidgetVisuals { fn from(value: WidgetStyle) -> Self { diff --git a/examples/hello_world_simple/Cargo.toml b/examples/hello_world_simple/Cargo.toml index d5ce91625..5b1146883 100644 --- a/examples/hello_world_simple/Cargo.toml +++ b/examples/hello_world_simple/Cargo.toml @@ -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" diff --git a/examples/hello_world_simple/src/ess_style_engine.rs b/examples/hello_world_simple/src/ess_style_engine.rs new file mode 100644 index 000000000..7e2df7285 --- /dev/null +++ b/examples/hello_world_simple/src/ess_style_engine.rs @@ -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; + +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 { + style: Arc, + wrapped_engine: E, + _stylable: std::marker::PhantomData, +} + +impl, T: Stylable> EssStyleEngine { + pub fn new(engine: E, style: Arc) -> Self { + Self { + style, + wrapped_engine: engine, + _stylable: std::marker::PhantomData, + } + } +} + +impl, T: Stylable + Sync + Send> StyleEngine for EssStyleEngine { + 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, + 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>, +} + +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 } + } +} diff --git a/examples/hello_world_simple/src/example_style.ess b/examples/hello_world_simple/src/example_style.ess new file mode 100644 index 000000000..b74e242d6 --- /dev/null +++ b/examples/hello_world_simple/src/example_style.ess @@ -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, + } +} diff --git a/examples/hello_world_simple/src/main.rs b/examples/hello_world_simple/src/main.rs index bb55bab28..15ac72052 100644 --- a/examples/hello_world_simple/src/main.rs +++ b/examples/hello_world_simple/src/main.rs @@ -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")); }); }) }