mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 04:40:03 -04:00
clean history
This commit is contained in:
45
Cargo.lock
45
Cargo.lock
@@ -2751,6 +2751,38 @@ version = "0.4.33"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
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]]
|
[[package]]
|
||||||
name = "lz4_flex"
|
name = "lz4_flex"
|
||||||
version = "0.13.1"
|
version = "0.13.1"
|
||||||
@@ -3966,9 +3998,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.4.8"
|
version = "0.4.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
|
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -4530,6 +4562,15 @@ dependencies = [
|
|||||||
"float-cmp",
|
"float-cmp",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "styling_engine"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"eframe",
|
||||||
|
"env_logger",
|
||||||
|
"logos",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
|
|||||||
@@ -35,9 +35,10 @@ use crate::{
|
|||||||
output::FullOutput,
|
output::FullOutput,
|
||||||
pass_state::PassState,
|
pass_state::PassState,
|
||||||
plugin::{self, TypedPluginHandle},
|
plugin::{self, TypedPluginHandle},
|
||||||
resize, response, scroll_area,
|
resize, response, scroll_area, theme_plugin,
|
||||||
util::IdTypeMap,
|
util::IdTypeMap,
|
||||||
viewport::ViewportClass,
|
viewport::ViewportClass,
|
||||||
|
widget_style::{Classes, StyleStruct, WidgetState},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::IdMap;
|
use crate::IdMap;
|
||||||
@@ -405,6 +406,8 @@ struct ContextImpl {
|
|||||||
is_accesskit_enabled: bool,
|
is_accesskit_enabled: bool,
|
||||||
|
|
||||||
loaders: Arc<Loaders>,
|
loaders: Arc<Loaders>,
|
||||||
|
|
||||||
|
themes: theme_plugin::Themes,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContextImpl {
|
impl ContextImpl {
|
||||||
@@ -2027,6 +2030,43 @@ impl Context {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Context {
|
||||||
|
/// Register a [`ThemeStyle`](crate::theme_plugin::ThemeStyle) for the specified widget [`StyleStruct`](StyleStruct) `S`
|
||||||
|
///
|
||||||
|
/// A theme can only be added once for a specified widget.
|
||||||
|
/// This way it's convenient to add themes in `eframe::run_simple_native`.
|
||||||
|
/// If you want to add the theme anyway, use [`Self::replace_theme`] instead.
|
||||||
|
pub fn add_theme<S: StyleStruct + 'static>(
|
||||||
|
&self,
|
||||||
|
theme: impl theme_plugin::ThemeStyle<S> + Send + Sync + 'static,
|
||||||
|
) {
|
||||||
|
self.write(|ctx| ctx.themes.register::<S>(theme, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a [`ThemeStyle`](crate::theme_plugin::ThemeStyle) for the specified widget [`StyleStruct`](StyleStruct) `S`
|
||||||
|
///
|
||||||
|
/// Overwrite any theme already registered for the specified widget [`StyleStruct`](StyleStruct).
|
||||||
|
/// If you want to avoid overwriting existing theme, use [`Self::add_theme`] instead.
|
||||||
|
pub fn replace_theme<S: StyleStruct + 'static>(
|
||||||
|
&self,
|
||||||
|
theme: impl theme_plugin::ThemeStyle<S> + Send + Sync + 'static,
|
||||||
|
) {
|
||||||
|
self.write(|ctx| ctx.themes.register::<S>(theme, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the [`StyleStruct`] using the registered theme if available.
|
||||||
|
///
|
||||||
|
/// Return `None` if no theme was registered for the [`StyleStruct`]
|
||||||
|
pub fn get_style<S: StyleStruct + Clone + 'static>(
|
||||||
|
&self,
|
||||||
|
classes: &Classes,
|
||||||
|
state: WidgetState,
|
||||||
|
base: &Style,
|
||||||
|
) -> Option<S> {
|
||||||
|
self.write(move |ctx| ctx.themes.get::<S>(classes, state, base))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
/// Tell `egui` which fonts to use.
|
/// Tell `egui` which fonts to use.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -417,6 +417,7 @@ pub mod response;
|
|||||||
mod sense;
|
mod sense;
|
||||||
pub mod style;
|
pub mod style;
|
||||||
pub mod text_selection;
|
pub mod text_selection;
|
||||||
|
pub mod theme_plugin;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod ui_builder;
|
mod ui_builder;
|
||||||
mod ui_stack;
|
mod ui_stack;
|
||||||
|
|||||||
113
crates/egui/src/theme_plugin.rs
Normal file
113
crates/egui/src/theme_plugin.rs
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use epaint::mutex::Mutex;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Id, Style, Ui,
|
||||||
|
util::IdTypeMap,
|
||||||
|
widget_style::{Classes, StyleStruct, WidgetState},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A cache that can be implemented to reduce computation time of a `ThemeStyle`
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct ThemeCache {
|
||||||
|
cache: IdTypeMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThemeCache {
|
||||||
|
/// Access the cache for the requested [`StyleStruct`] based on the [`Classes`] and
|
||||||
|
/// the [`WidgetState`]
|
||||||
|
///
|
||||||
|
/// If no entry match the parameter then compute the fallback style and
|
||||||
|
/// save the output for later.
|
||||||
|
pub fn get<S: StyleStruct + 'static>(
|
||||||
|
&mut self,
|
||||||
|
classes: &Classes,
|
||||||
|
state: WidgetState,
|
||||||
|
fallback: impl FnOnce() -> S,
|
||||||
|
) -> S {
|
||||||
|
let style_id = Id::new(classes).with(state);
|
||||||
|
if let Some(style) = self.cache.get_temp::<S>(style_id) {
|
||||||
|
style
|
||||||
|
} else {
|
||||||
|
let style = fallback();
|
||||||
|
self.cache.insert_temp(style_id, style.clone());
|
||||||
|
style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Theme plugin that implement a style computation for a defined `StyleStruct`
|
||||||
|
pub trait ThemeStyle<S> {
|
||||||
|
/// The style according to the classes and state of the widget
|
||||||
|
fn style(&mut self, classes: &Classes, state: WidgetState, base: &Style) -> S;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ui {
|
||||||
|
/// Access the installed theme plugin if there is one and fetch the requested widget style if it exist.
|
||||||
|
/// Fallback to the default style if not found.
|
||||||
|
///
|
||||||
|
/// Requested widget style must implement [`StyleStruct`].
|
||||||
|
pub fn widget_style<S: StyleStruct + Clone + 'static>(
|
||||||
|
&self,
|
||||||
|
id: crate::Id,
|
||||||
|
classes: &Classes,
|
||||||
|
) -> S {
|
||||||
|
// If the requested `StyleStruct` is cached, return it without computing.
|
||||||
|
// Otherwise proceed to compute the style from the widget information.
|
||||||
|
|
||||||
|
// Fetch the current state of the widget
|
||||||
|
let state = self
|
||||||
|
.ctx()
|
||||||
|
.read_response(id)
|
||||||
|
.map(|r| r.widget_state())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if let Some(style) = self.get_style::<S>(classes, state, self.style()) {
|
||||||
|
style
|
||||||
|
} else {
|
||||||
|
S::default_style(classes, state, self.style())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) struct Themes {
|
||||||
|
themes: IdTypeMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Themes {
|
||||||
|
/// Register a theme and the style associated
|
||||||
|
pub(crate) fn register<S: StyleStruct + 'static>(
|
||||||
|
&mut self,
|
||||||
|
theme: impl ThemeStyle<S> + Send + Sync + 'static,
|
||||||
|
force: bool,
|
||||||
|
) {
|
||||||
|
if !force
|
||||||
|
&& self
|
||||||
|
.themes
|
||||||
|
.get_temp::<Arc<Mutex<Box<dyn ThemeStyle<S> + Send + Sync>>>>(Id::NULL)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.themes
|
||||||
|
.insert_temp::<Arc<Mutex<Box<dyn ThemeStyle<S> + Send + Sync>>>>(
|
||||||
|
Id::NULL,
|
||||||
|
Arc::new(Mutex::new(Box::new(theme))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch the style of the current theme
|
||||||
|
pub(crate) fn get<S: StyleStruct + 'static>(
|
||||||
|
&self,
|
||||||
|
classes: &Classes,
|
||||||
|
state: WidgetState,
|
||||||
|
base: &Style,
|
||||||
|
) -> Option<S> {
|
||||||
|
let v = self
|
||||||
|
.themes
|
||||||
|
.get_temp::<Arc<Mutex<Box<dyn ThemeStyle<S> + Send + Sync>>>>(Id::NULL);
|
||||||
|
v.map(|engine| engine.lock().style(classes, state, base))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
use std::{borrow::Cow, fmt};
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
fmt::{self, Debug},
|
||||||
|
};
|
||||||
|
|
||||||
use emath::Vec2;
|
use emath::Vec2;
|
||||||
use epaint::{Color32, FontId, Shadow, Stroke, text::TextWrapMode};
|
use epaint::{Color32, FontId, Shadow, Stroke, text::TextWrapMode};
|
||||||
@@ -9,7 +12,15 @@ use crate::{
|
|||||||
style::{WidgetVisuals, Widgets},
|
style::{WidgetVisuals, Widgets},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Each dedicated style must implement this trait to be used in the theme plugin system
|
||||||
|
pub trait StyleStruct: Debug + Clone + Send + Sync + std::any::Any + 'static {
|
||||||
|
fn default_style(classes: &Classes, state: WidgetState, base: &Style) -> Self
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
}
|
||||||
|
|
||||||
/// General text style
|
/// General text style
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct TextVisuals {
|
pub struct TextVisuals {
|
||||||
/// Font used
|
/// Font used
|
||||||
pub font_id: FontId,
|
pub font_id: FontId,
|
||||||
@@ -23,6 +34,7 @@ pub struct TextVisuals {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// General widget style
|
/// General widget style
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct WidgetStyle {
|
pub struct WidgetStyle {
|
||||||
pub frame: Frame,
|
pub frame: Frame,
|
||||||
|
|
||||||
@@ -31,13 +43,69 @@ pub struct WidgetStyle {
|
|||||||
pub stroke: Stroke,
|
pub stroke: Stroke,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl StyleStruct for WidgetStyle {
|
||||||
|
fn default_style(_classes: &Classes, state: WidgetState, base: &Style) -> Self {
|
||||||
|
let visuals = base.visuals.widgets.state(state);
|
||||||
|
let font_id = base.override_font_id.clone();
|
||||||
|
Self {
|
||||||
|
frame: Frame {
|
||||||
|
fill: visuals.bg_fill,
|
||||||
|
stroke: visuals.bg_stroke,
|
||||||
|
corner_radius: visuals.corner_radius,
|
||||||
|
inner_margin: base.spacing.button_padding.into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
stroke: visuals.fg_stroke,
|
||||||
|
text: TextVisuals {
|
||||||
|
color: base
|
||||||
|
.visuals
|
||||||
|
.override_text_color
|
||||||
|
.unwrap_or_else(|| visuals.text_color()),
|
||||||
|
font_id: font_id.unwrap_or_else(|| TextStyle::Body.resolve(base)),
|
||||||
|
strikethrough: Stroke::NONE,
|
||||||
|
underline: Stroke::NONE,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Dedicated button style
|
/// Dedicated button style
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct ButtonStyle {
|
pub struct ButtonStyle {
|
||||||
pub frame: Frame,
|
pub frame: Frame,
|
||||||
pub text_style: TextVisuals,
|
pub text_style: TextVisuals,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl StyleStruct for ButtonStyle {
|
||||||
|
fn default_style(classes: &Classes, state: WidgetState, base: &Style) -> Self {
|
||||||
|
let mut visuals = *base.visuals.widgets.state(state);
|
||||||
|
let mut ws = WidgetStyle::default_style(classes, state, base);
|
||||||
|
|
||||||
|
if classes.has(SELECTED_CLASS) {
|
||||||
|
visuals.weak_bg_fill = base.visuals.selection.bg_fill;
|
||||||
|
visuals.bg_fill = base.visuals.selection.bg_fill;
|
||||||
|
visuals.fg_stroke = base.visuals.selection.stroke;
|
||||||
|
ws.text.color = base.visuals.selection.stroke.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
frame: Frame {
|
||||||
|
fill: visuals.weak_bg_fill,
|
||||||
|
stroke: visuals.bg_stroke,
|
||||||
|
corner_radius: visuals.corner_radius,
|
||||||
|
outer_margin: (-Vec2::splat(visuals.expansion)).into(),
|
||||||
|
inner_margin: (base.spacing.button_padding + Vec2::splat(visuals.expansion)
|
||||||
|
- Vec2::splat(visuals.bg_stroke.width))
|
||||||
|
.into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
text_style: ws.text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Dedicated checkbox style
|
/// Dedicated checkbox style
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct CheckboxStyle {
|
pub struct CheckboxStyle {
|
||||||
/// Frame around
|
/// Frame around
|
||||||
pub frame: Frame,
|
pub frame: Frame,
|
||||||
@@ -58,7 +126,28 @@ pub struct CheckboxStyle {
|
|||||||
pub check_stroke: Stroke,
|
pub check_stroke: Stroke,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl StyleStruct for CheckboxStyle {
|
||||||
|
fn default_style(classes: &Classes, state: WidgetState, base: &Style) -> Self {
|
||||||
|
let visuals = base.visuals.widgets.state(state);
|
||||||
|
let ws = WidgetStyle::default_style(classes, state, base);
|
||||||
|
Self {
|
||||||
|
frame: Frame::new(),
|
||||||
|
checkbox_size: base.spacing.icon_width,
|
||||||
|
check_size: base.spacing.icon_width_inner,
|
||||||
|
checkbox_frame: Frame {
|
||||||
|
fill: visuals.bg_fill,
|
||||||
|
corner_radius: visuals.corner_radius,
|
||||||
|
stroke: visuals.bg_stroke,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
text_style: ws.text,
|
||||||
|
check_stroke: ws.stroke,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Dedicated label style
|
/// Dedicated label style
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct LabelStyle {
|
pub struct LabelStyle {
|
||||||
/// Frame around
|
/// Frame around
|
||||||
pub frame: Frame,
|
pub frame: Frame,
|
||||||
@@ -71,6 +160,7 @@ pub struct LabelStyle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Dedicated separator style
|
/// Dedicated separator style
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct SeparatorStyle {
|
pub struct SeparatorStyle {
|
||||||
/// How much space is allocated in the layout direction
|
/// How much space is allocated in the layout direction
|
||||||
pub spacing: f32,
|
pub spacing: f32,
|
||||||
@@ -80,7 +170,7 @@ pub struct SeparatorStyle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The different state of a widget can be
|
/// The different state of a widget can be
|
||||||
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
pub enum WidgetState {
|
pub enum WidgetState {
|
||||||
Noninteractive,
|
Noninteractive,
|
||||||
#[default]
|
#[default]
|
||||||
@@ -231,7 +321,7 @@ pub type ClassName = Cow<'static, str>;
|
|||||||
///
|
///
|
||||||
/// This can be used by styling engine to compute a different style
|
/// This can be used by styling engine to compute a different style
|
||||||
/// based on the set of classes present on the widget/Ui.
|
/// based on the set of classes present on the widget/Ui.
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone, Hash)]
|
||||||
pub struct Classes {
|
pub struct Classes {
|
||||||
classes: SmallVec<[ClassName; 5]>,
|
classes: SmallVec<[ClassName; 5]>,
|
||||||
}
|
}
|
||||||
@@ -315,4 +405,9 @@ pub trait HasClasses {
|
|||||||
fn has(&self, class: impl Into<ClassName>) -> bool {
|
fn has(&self, class: impl Into<ClassName>) -> bool {
|
||||||
self.classes().classes.contains(&class.into())
|
self.classes().classes.contains(&class.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The list of class
|
||||||
|
fn list(&self) -> Vec<ClassName> {
|
||||||
|
self.classes().classes.to_vec()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -328,7 +328,8 @@ impl<'a> Button<'a> {
|
|||||||
|
|
||||||
classes.add_class_if(SELECTED_CLASS, selected.unwrap_or(false));
|
classes.add_class_if(SELECTED_CLASS, selected.unwrap_or(false));
|
||||||
|
|
||||||
let ButtonStyle { frame, text_style } = ui.style().button_style(&classes, state);
|
// let ButtonStyle { frame, text_style } = ui.style().button_style(&classes, state);
|
||||||
|
let ButtonStyle { frame, text_style } = ui.widget_style(id, &classes);
|
||||||
|
|
||||||
let mut button_padding = if has_frame_margin {
|
let mut button_padding = if has_frame_margin {
|
||||||
frame.inner_margin
|
frame.inner_margin
|
||||||
|
|||||||
20
examples/styling_engine/Cargo.toml
Normal file
20
examples/styling_engine/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
[package]
|
||||||
|
name = "styling_engine"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
edition = "2024"
|
||||||
|
rust-version = "1.92"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
eframe = { workspace = true, features = [
|
||||||
|
"default",
|
||||||
|
"__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"
|
||||||
7
examples/styling_engine/README.md
Normal file
7
examples/styling_engine/README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Example showing how the style engine work.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run -p styling_engine
|
||||||
|
```
|
||||||
|
|
||||||
|
<!--  -->
|
||||||
140
examples/styling_engine/src/custom_engine.rs
Normal file
140
examples/styling_engine/src/custom_engine.rs
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use eframe::egui::{
|
||||||
|
Color32, Style,
|
||||||
|
theme_plugin::{ThemeCache, ThemeStyle},
|
||||||
|
widget_style::{
|
||||||
|
ButtonStyle, Classes, HasClasses as _, StyleStruct as _, WidgetState, WidgetStyle,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use logos::Logos;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct ESSEngine {
|
||||||
|
info: HashMap<String, Vec<(String, Value)>>,
|
||||||
|
cache: ThemeCache,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ESSEngine {
|
||||||
|
pub fn try_parse(ess: &str) -> Result<Self, String> {
|
||||||
|
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::<usize>()
|
||||||
|
.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 ThemeStyle<WidgetStyle> for ESSEngine {
|
||||||
|
fn style(&mut self, classes: &Classes, state: WidgetState, base: &Style) -> WidgetStyle {
|
||||||
|
self.cache
|
||||||
|
.get(classes, state, || base.widget_style(classes, state))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThemeStyle<ButtonStyle> for ESSEngine {
|
||||||
|
fn style(&mut self, classes: &Classes, state: WidgetState, base: &Style) -> ButtonStyle {
|
||||||
|
self.cache.get(classes, state, || {
|
||||||
|
let mut default = ButtonStyle::default_style(classes, state, base);
|
||||||
|
for classe in classes.list() {
|
||||||
|
if let Some(properties) = self.info.get(&classe.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),
|
||||||
|
}
|
||||||
106
examples/styling_engine/src/main.rs
Normal file
106
examples/styling_engine/src/main.rs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
#![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
|
||||||
|
|
||||||
|
use eframe::egui::widget_style::WidgetStyle;
|
||||||
|
use eframe::egui::{
|
||||||
|
self, Button, Frame, Margin, Panel, UiBuilder,
|
||||||
|
widget_style::{ButtonStyle, HasClasses as _},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::custom_engine::ESSEngine;
|
||||||
|
|
||||||
|
mod custom_engine;
|
||||||
|
|
||||||
|
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]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut style_code = "
|
||||||
|
.red {
|
||||||
|
fill: #f00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blue {
|
||||||
|
fill: #00f;
|
||||||
|
border: 10;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
|
let mut toggled = false;
|
||||||
|
|
||||||
|
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_theme::<WidgetStyle>(engine.clone());
|
||||||
|
ui.add_theme::<ButtonStyle>(engine);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.scope_builder(UiBuilder::new().with_class("body"), |ui| {
|
||||||
|
ui.label("body");
|
||||||
|
ui.label("central panel");
|
||||||
|
|
||||||
|
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)",
|
||||||
|
);
|
||||||
|
|
||||||
|
if ui.text_edit_multiline(&mut style_code).changed()
|
||||||
|
&& let Ok(engine) = ESSEngine::try_parse(&style_code)
|
||||||
|
{
|
||||||
|
ui.replace_theme::<WidgetStyle>(engine.clone());
|
||||||
|
ui.replace_theme::<ButtonStyle>(engine);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should find a way to detect if a change is made on a plugin
|
||||||
|
// Maybe by saving the plugin in the system instead and invalidating the cache when it's pulled as mut ?
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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")
|
||||||
|
));
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
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"));
|
||||||
|
if ui
|
||||||
|
.add(
|
||||||
|
Button::new("red/blue")
|
||||||
|
.with_class_if("red", toggled)
|
||||||
|
.with_class_if("blue", !toggled),
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
toggled = !toggled;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
8
examples/styling_engine/src/style.ess
Normal file
8
examples/styling_engine/src/style.ess
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.red {
|
||||||
|
fill: #f00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blue {
|
||||||
|
fill: #00f;
|
||||||
|
border: 4;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user