diff --git a/Cargo.lock b/Cargo.lock index 1983d6efa..0f289e5a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2715,38 +2715,6 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "lz4_flex" version = "0.13.1" @@ -4520,7 +4488,6 @@ version = "0.1.0" dependencies = [ "eframe", "env_logger", - "logos", ] [[package]] diff --git a/examples/styling_engine/Cargo.toml b/examples/styling_engine/Cargo.toml index d194d8e0a..19cec8ae9 100644 --- a/examples/styling_engine/Cargo.toml +++ b/examples/styling_engine/Cargo.toml @@ -17,4 +17,3 @@ eframe = { workspace = true, features = [ "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO ] } env_logger = { workspace = true, features = ["auto-color", "humantime"] } -logos = "0.16.1" diff --git a/examples/styling_engine/README.md b/examples/styling_engine/README.md index f2cc8721c..43f54f5bb 100644 --- a/examples/styling_engine/README.md +++ b/examples/styling_engine/README.md @@ -1,5 +1,8 @@ Example showing how the style engine work. +A custom `StyleProvider` styles every button, using _classes_ to tell them apart. +The theme can be edited live in the left panel. + ```sh cargo run -p styling_engine ``` diff --git a/examples/styling_engine/src/custom_engine.rs b/examples/styling_engine/src/custom_engine.rs deleted file mode 100644 index f5e854a89..000000000 --- a/examples/styling_engine/src/custom_engine.rs +++ /dev/null @@ -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>, -} - -impl ESSEngine { - pub fn try_parse(ess: &str) -> Result { - 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::() - .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 for ESSEngine { - fn style(&mut self, modifiers: &StyleArgs<'_>) -> ButtonStyle { - let StyleArgs { classes, ctx, .. } = modifiers; - let base = ctx.get_widget_style::(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), -} diff --git a/examples/styling_engine/src/main.rs b/examples/styling_engine/src/main.rs index 3e2222f50..d9503a3e6 100644 --- a/examples/styling_engine/src/main.rs +++ b/examples/styling_engine/src/main.rs @@ -1,102 +1,164 @@ #![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 +//! 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::{ - self, Button, Frame, Margin, Panel, UiBuilder, - theme::ThemeCache, - widget_style::{ButtonStyle, HasClasses as _}, + self, CentralPanel, Color32, Frame, Panel, + theme::StyleProvider, + 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 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 { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). 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() }; - let mut style_code = " -.red { - fill: #f00; -} + let mut preset = Preset::Ocean; + let mut theme = MyTheme::preset(preset); + let mut last_click = "nothing"; -.blue { - fill: #00f; - border: 10; -} -" - .to_owned(); + eframe::run_ui_native("Styling engine", options, move |ui, _frame| { + // Register our theme for all buttons. This is a no-op after the first frame. + // Wrap it in a `ThemeCache` if computing the style is expensive. + ui.add_widget_theme::(theme); - 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| { - // Register the theme plugin and which style they implement - if let Ok(engine) = ESSEngine::try_parse(&style_code) { - ui.add_widget_theme::(ThemeCache::new(engine)); - } + ui.separator(); - ui.scope_builder(UiBuilder::new().with_class("body"), |ui| { - ui.label("body"); - ui.label("central panel"); + ui.heading("Button theme"); - Panel::left("style_code").show(ui, |ui| { - 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)", - ); + let mut changed = false; - if ui.text_edit_multiline(&mut style_code).changed() - && let Ok(engine) = ESSEngine::try_parse(&style_code) - { - // Overwrite the current theme with the new one.clear - ui.replace_widget_theme::(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") - )); - }) + egui::ComboBox::from_label("Preset") + .selected_text(match preset { + Preset::Ocean => "Ocean", + Preset::Candy => "Candy", }) - }); + .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.add(Button::new("red").with_class("red")); - ui.add(Button::new("blue").with_class("blue")); - ui.add(Button::new("dynamic in engine A").with_class("dynamic")); + ui.horizontal(|ui| { + changed |= ui.color_edit_button_srgba(&mut theme.normal).changed(); + ui.label("Normal"); + }); + 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::(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 - .add( - Button::new("red/blue") - .with_class_if("red", toggled) - .with_class_if("blue", !toggled), - ) + .add(egui::Button::new("Delete everything").with_class(DANGER)) .clicked() { - toggled = !toggled; + last_click = "Delete everything"; } + + ui.add_space(8.0); + ui.label(format!("Last clicked: {last_click}")); }); }) } diff --git a/examples/styling_engine/src/style.ess b/examples/styling_engine/src/style.ess deleted file mode 100644 index 24358af1e..000000000 --- a/examples/styling_engine/src/style.ess +++ /dev/null @@ -1,8 +0,0 @@ -.red { - fill: #f00; -} - -.blue { - fill: #00f; - border: 4; -}