mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 04:40:03 -04:00
Theme plugin system (experimental) (#8153)
<!-- Please read the "Making a PR" section of [`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md) before opening a Pull Request! * Keep your PR:s small and focused. * The PR title is what ends up in the changelog, so make it descriptive! * If applicable, add a screenshot or gif. * If it is a non-trivial addition, consider adding a demo for it to `egui_demo_lib`, or a new example. * Do NOT open PR:s from your `master` branch, as that makes it hard for maintainers to test and add commits to your PR. * Remember to run `cargo fmt` and `cargo clippy`. * Open the PR as a draft until you have self-reviewed it and run `./scripts/check.sh`. * When you have addressed a PR comment, mark it as resolved. Please be patient! I will review your PR, but my time is limited! --> # What it does Addition of a new system of theme plugins which allow the user to use different rules engine to compute the style for the available specialised widget style. # How to use Create a engine implementing the trait `ThemePlugin` and `ThemeStyle<S: StyleStruct>` and implement the necessary methods, then register this way (example for `ButtonStyle`): ```ui.add_theme::<ButtonStyle>(&mycustomengine);``` Now all button will call the `ThemeStyle<ButtonStyle>` method to compute the correct style and later use the cached value to avoid the costly computation. If no valid `ThemeStyle<S>` or engine is available then it fallback to the default style. * Closes part of <https://github.com/emilk/egui/issues/3284> * [x] I have followed the instructions in the PR template --------- Co-authored-by: adrien <221212@umons.ac.be> Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Lucas Meurer <hi@lucasmerlin.me>
This commit is contained in:
@@ -4479,6 +4479,14 @@ dependencies = [
|
|||||||
"float-cmp",
|
"float-cmp",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "styling_engine"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"eframe",
|
||||||
|
"env_logger",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
|
|||||||
@@ -53,6 +53,12 @@ android-native-activity = ["egui-winit/android-native-activity"]
|
|||||||
## If you plan on specifying your own fonts you may disable this feature.
|
## If you plan on specifying your own fonts you may disable this feature.
|
||||||
default_fonts = ["egui/default_fonts"]
|
default_fonts = ["egui/default_fonts"]
|
||||||
|
|
||||||
|
## Enable experimental egui features that might have massive breaking changes or be removed entirely in future updates.
|
||||||
|
## Enabling this won't break semver, it's just future compatibility risk.
|
||||||
|
##
|
||||||
|
## Currently, this enables the theme plugin.
|
||||||
|
experimental = ["egui/experimental"]
|
||||||
|
|
||||||
## Enable [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow).
|
## Enable [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow).
|
||||||
##
|
##
|
||||||
## There is generally no need to enable both the `wgpu` and `glow` features,
|
## There is generally no need to enable both the `wgpu` and `glow` features,
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ color-hex = ["epaint/color-hex"]
|
|||||||
## If you plan on specifying your own fonts you may disable this feature.
|
## If you plan on specifying your own fonts you may disable this feature.
|
||||||
default_fonts = ["epaint/default_fonts"]
|
default_fonts = ["epaint/default_fonts"]
|
||||||
|
|
||||||
|
## Enable experimental egui features that might have massive breaking changes or be removed entirely in future updates.
|
||||||
|
## Enabling this won't break semver, it's just future compatibility risk.
|
||||||
|
##
|
||||||
|
## Currently, this enables the theme plugin.
|
||||||
|
experimental = []
|
||||||
|
|
||||||
## [`mint`](https://docs.rs/mint) enables interoperability with other math libraries such as [`glam`](https://docs.rs/glam) and [`nalgebra`](https://docs.rs/nalgebra).
|
## [`mint`](https://docs.rs/mint) enables interoperability with other math libraries such as [`glam`](https://docs.rs/glam) and [`nalgebra`](https://docs.rs/nalgebra).
|
||||||
mint = ["epaint/mint"]
|
mint = ["epaint/mint"]
|
||||||
|
|
||||||
|
|||||||
@@ -36,9 +36,10 @@ use crate::{
|
|||||||
output::{FullOutput, LogicOutput},
|
output::{FullOutput, LogicOutput},
|
||||||
pass_state::PassState,
|
pass_state::PassState,
|
||||||
plugin::{self, TypedPluginHandle},
|
plugin::{self, TypedPluginHandle},
|
||||||
resize, response, scroll_area,
|
resize, response, scroll_area, theme,
|
||||||
util::IdTypeMap,
|
util::IdTypeMap,
|
||||||
viewport::ViewportClass,
|
viewport::ViewportClass,
|
||||||
|
widget_style::{StyleArgs, WidgetStyle},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::IdMap;
|
use crate::IdMap;
|
||||||
@@ -412,6 +413,8 @@ struct ContextImpl {
|
|||||||
is_accesskit_enabled: bool,
|
is_accesskit_enabled: bool,
|
||||||
|
|
||||||
loaders: Arc<Loaders>,
|
loaders: Arc<Loaders>,
|
||||||
|
|
||||||
|
themes: theme::Themes,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContextImpl {
|
impl ContextImpl {
|
||||||
@@ -2093,6 +2096,48 @@ impl Context {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Experimental theming, gated behind the `experimental_theme` feature.
|
||||||
|
impl Context {
|
||||||
|
/// Register a [`StyleProvider`](crate::theme::StyleProvider) for the specified widget type.
|
||||||
|
///
|
||||||
|
/// A theme can only be added once for a specified widget.
|
||||||
|
/// If a theme is already registered for this widget, this is a no-op (useful for `eframe::run_simple_native`).
|
||||||
|
///
|
||||||
|
/// If you want to add the theme anyway, use [`Self::replace_widget_theme`] instead.
|
||||||
|
#[cfg(feature = "experimental")]
|
||||||
|
pub fn add_widget_theme<S: WidgetStyle + 'static>(
|
||||||
|
&self,
|
||||||
|
theme: impl theme::StyleProvider<S> + Send + Sync + 'static,
|
||||||
|
) {
|
||||||
|
self.write(|ctx| ctx.themes.register::<S>(theme, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a [`StyleProvider`](crate::theme::StyleProvider) for the specified widget.
|
||||||
|
///
|
||||||
|
/// Overwrite any theme already registered for the specified widget [`WidgetStyle`].
|
||||||
|
/// This allow to live edit a theme.
|
||||||
|
#[cfg(feature = "experimental")]
|
||||||
|
pub fn replace_widget_theme<S: WidgetStyle + 'static>(
|
||||||
|
&self,
|
||||||
|
theme: impl theme::StyleProvider<S> + Send + Sync + 'static,
|
||||||
|
) {
|
||||||
|
self.write(|ctx| ctx.themes.register::<S>(theme, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the `WidgetStyle` using the registered theme.
|
||||||
|
///
|
||||||
|
/// The types you need to call this (e.g. `StyleArgs`) are only public
|
||||||
|
/// with the `experimental_theme` feature.
|
||||||
|
#[cfg_attr(not(feature = "experimental"), doc(hidden))]
|
||||||
|
pub fn get_widget_style<S: WidgetStyle + Clone + 'static>(
|
||||||
|
&self,
|
||||||
|
modifiers: &StyleArgs<'_>,
|
||||||
|
) -> S {
|
||||||
|
let theme = self.read(move |ctx| ctx.themes.get::<S>());
|
||||||
|
theme.lock().style(modifiers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
/// Tell `egui` which fonts to use.
|
/// Tell `egui` which fonts to use.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -417,13 +417,20 @@ pub mod response;
|
|||||||
mod sense;
|
mod sense;
|
||||||
pub mod style;
|
pub mod style;
|
||||||
pub mod text_selection;
|
pub mod text_selection;
|
||||||
|
#[cfg(feature = "experimental")]
|
||||||
|
pub mod theme;
|
||||||
|
#[cfg(not(feature = "experimental"))]
|
||||||
|
mod theme;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod ui_builder;
|
mod ui_builder;
|
||||||
mod ui_stack;
|
mod ui_stack;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
pub mod viewport;
|
pub mod viewport;
|
||||||
mod widget_rect;
|
mod widget_rect;
|
||||||
|
#[cfg(feature = "experimental")]
|
||||||
pub mod widget_style;
|
pub mod widget_style;
|
||||||
|
#[cfg(not(feature = "experimental"))]
|
||||||
|
mod widget_style;
|
||||||
pub mod widget_text;
|
pub mod widget_text;
|
||||||
pub mod widgets;
|
pub mod widgets;
|
||||||
|
|
||||||
|
|||||||
156
crates/egui/src/theme/default_style.rs
Normal file
156
crates/egui/src/theme/default_style.rs
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
use emath::Vec2;
|
||||||
|
use epaint::{Shadow, Stroke, text::TextWrapMode};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Frame, TextStyle,
|
||||||
|
theme::StyleProvider,
|
||||||
|
widget_style::{
|
||||||
|
BaseStyle, ButtonStyle, CheckboxStyle, HasClasses as _, LabelStyle, SELECTED_CLASS,
|
||||||
|
SeparatorStyle, StyleArgs, TextVisuals, WidgetState,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The default [`StyleProvider`], implementing the default egui look based on
|
||||||
|
/// [`crate::style::WidgetVisuals`].
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DefaultStyle;
|
||||||
|
|
||||||
|
impl StyleProvider<BaseStyle> for DefaultStyle {
|
||||||
|
fn style(&mut self, modifiers: &StyleArgs<'_>) -> BaseStyle {
|
||||||
|
let StyleArgs { style, state, .. } = modifiers;
|
||||||
|
let spacing = &style.spacing;
|
||||||
|
let widget_visuals = match state {
|
||||||
|
WidgetState::Noninteractive => style.visuals.widgets.noninteractive,
|
||||||
|
WidgetState::Inactive => style.visuals.widgets.inactive,
|
||||||
|
WidgetState::Hovered => style.visuals.widgets.hovered,
|
||||||
|
WidgetState::Active => style.visuals.widgets.active,
|
||||||
|
};
|
||||||
|
|
||||||
|
BaseStyle {
|
||||||
|
frame: Frame {
|
||||||
|
fill: widget_visuals.bg_fill,
|
||||||
|
stroke: widget_visuals.bg_stroke,
|
||||||
|
corner_radius: widget_visuals.corner_radius,
|
||||||
|
inner_margin: spacing.button_padding.into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
stroke: widget_visuals.fg_stroke,
|
||||||
|
text: TextVisuals {
|
||||||
|
color: widget_visuals.text_color(),
|
||||||
|
font_id: modifiers
|
||||||
|
.style
|
||||||
|
.override_font_id
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| TextStyle::Body.resolve(style)),
|
||||||
|
strikethrough: Stroke::NONE,
|
||||||
|
underline: Stroke::NONE,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StyleProvider<ButtonStyle> for DefaultStyle {
|
||||||
|
fn style(&mut self, modifiers: &StyleArgs<'_>) -> ButtonStyle {
|
||||||
|
let StyleArgs {
|
||||||
|
ctx,
|
||||||
|
classes,
|
||||||
|
style,
|
||||||
|
state,
|
||||||
|
..
|
||||||
|
} = modifiers;
|
||||||
|
let spacing = &style.spacing;
|
||||||
|
let mut widget_visuals = match state {
|
||||||
|
WidgetState::Noninteractive => style.visuals.widgets.noninteractive,
|
||||||
|
WidgetState::Inactive => style.visuals.widgets.inactive,
|
||||||
|
WidgetState::Hovered => style.visuals.widgets.hovered,
|
||||||
|
WidgetState::Active => style.visuals.widgets.active,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ws: BaseStyle = ctx.get_widget_style(modifiers);
|
||||||
|
|
||||||
|
if classes.has(SELECTED_CLASS) {
|
||||||
|
let visuals = &style.visuals;
|
||||||
|
widget_visuals.weak_bg_fill = visuals.selection.bg_fill;
|
||||||
|
widget_visuals.bg_fill = visuals.selection.bg_fill;
|
||||||
|
widget_visuals.fg_stroke = visuals.selection.stroke;
|
||||||
|
ws.text.color = visuals.selection.stroke.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
ButtonStyle {
|
||||||
|
frame: Frame {
|
||||||
|
fill: widget_visuals.weak_bg_fill,
|
||||||
|
stroke: widget_visuals.bg_stroke,
|
||||||
|
corner_radius: widget_visuals.corner_radius,
|
||||||
|
outer_margin: (-Vec2::splat(widget_visuals.expansion)).into(),
|
||||||
|
inner_margin: (spacing.button_padding + Vec2::splat(widget_visuals.expansion)
|
||||||
|
- Vec2::splat(widget_visuals.bg_stroke.width))
|
||||||
|
.into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
text_style: ws.text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StyleProvider<CheckboxStyle> for DefaultStyle {
|
||||||
|
fn style(&mut self, modifiers: &StyleArgs<'_>) -> CheckboxStyle {
|
||||||
|
let StyleArgs {
|
||||||
|
ctx, style, state, ..
|
||||||
|
} = modifiers;
|
||||||
|
let spacing = &style.spacing;
|
||||||
|
let widget_visuals = match state {
|
||||||
|
WidgetState::Noninteractive => style.visuals.widgets.noninteractive,
|
||||||
|
WidgetState::Inactive => style.visuals.widgets.inactive,
|
||||||
|
WidgetState::Hovered => style.visuals.widgets.hovered,
|
||||||
|
WidgetState::Active => style.visuals.widgets.active,
|
||||||
|
};
|
||||||
|
|
||||||
|
let ws: BaseStyle = ctx.get_widget_style(modifiers);
|
||||||
|
|
||||||
|
CheckboxStyle {
|
||||||
|
frame: Frame::new(),
|
||||||
|
checkbox_size: spacing.icon_width,
|
||||||
|
check_size: spacing.icon_width_inner,
|
||||||
|
checkbox_frame: Frame {
|
||||||
|
fill: widget_visuals.bg_fill,
|
||||||
|
corner_radius: widget_visuals.corner_radius,
|
||||||
|
stroke: widget_visuals.bg_stroke,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
text_style: ws.text,
|
||||||
|
check_stroke: ws.stroke,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StyleProvider<LabelStyle> for DefaultStyle {
|
||||||
|
fn style(&mut self, modifiers: &StyleArgs<'_>) -> LabelStyle {
|
||||||
|
let StyleArgs { ctx, .. } = modifiers;
|
||||||
|
let ws: BaseStyle = ctx.get_widget_style(modifiers);
|
||||||
|
|
||||||
|
LabelStyle {
|
||||||
|
frame: Frame {
|
||||||
|
fill: ws.frame.fill,
|
||||||
|
inner_margin: 0.0.into(),
|
||||||
|
outer_margin: 0.0.into(),
|
||||||
|
stroke: Stroke::NONE,
|
||||||
|
shadow: Shadow::NONE,
|
||||||
|
corner_radius: 0.into(),
|
||||||
|
},
|
||||||
|
text: ws.text,
|
||||||
|
wrap_mode: TextWrapMode::Wrap,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StyleProvider<SeparatorStyle> for DefaultStyle {
|
||||||
|
fn style(&mut self, modifiers: &StyleArgs<'_>) -> SeparatorStyle {
|
||||||
|
let StyleArgs { style, .. } = modifiers;
|
||||||
|
|
||||||
|
SeparatorStyle {
|
||||||
|
spacing: 6.0,
|
||||||
|
// A separator is never interactive, so its stroke doesn't depend on the widget state:
|
||||||
|
stroke: style.visuals.widgets.noninteractive.bg_stroke,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
crates/egui/src/theme/mod.rs
Normal file
48
crates/egui/src/theme/mod.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
//! Theming: pluggable [`StyleProvider`]s that compute the style of each widget.
|
||||||
|
|
||||||
|
// This module is only public with the `experimental_theme` feature,
|
||||||
|
// so without it a lot of it looks unused:
|
||||||
|
#![cfg_attr(not(feature = "experimental"), allow(dead_code, unused_imports))]
|
||||||
|
|
||||||
|
mod default_style;
|
||||||
|
mod style_provider;
|
||||||
|
mod themes;
|
||||||
|
|
||||||
|
pub use self::{default_style::DefaultStyle, style_provider::StyleProvider, themes::Themes};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Ui,
|
||||||
|
widget_style::{Classes, StyleArgs, WidgetState, WidgetStyle},
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Ui {
|
||||||
|
/// The style of the widget with the given [`crate::Id`] and `Classes`,
|
||||||
|
/// as computed by the registered theme.
|
||||||
|
///
|
||||||
|
/// The types you need to call this are only public with the
|
||||||
|
/// `experimental_theme` feature.
|
||||||
|
#[cfg_attr(not(feature = "experimental"), doc(hidden))]
|
||||||
|
pub fn widget_style<S: WidgetStyle + Clone + 'static>(
|
||||||
|
&self,
|
||||||
|
id: crate::Id,
|
||||||
|
classes: &Classes,
|
||||||
|
) -> S {
|
||||||
|
// Fetch the state of the widget, as it was in the previous pass
|
||||||
|
let state = if let Some(response) = self.read_response(id) {
|
||||||
|
response.widget_state()
|
||||||
|
} else {
|
||||||
|
// We don't know the state of the widget yet, so we would style it wrong.
|
||||||
|
// It will be styled correctly on next frame.
|
||||||
|
self.ctx().request_repaint();
|
||||||
|
WidgetState::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
self.get_widget_style::<S>(&StyleArgs {
|
||||||
|
classes,
|
||||||
|
state,
|
||||||
|
style: self.style(),
|
||||||
|
stack: self.stack(),
|
||||||
|
ctx: self,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
17
crates/egui/src/theme/style_provider.rs
Normal file
17
crates/egui/src/theme/style_provider.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
use core::any::TypeId;
|
||||||
|
|
||||||
|
use crate::widget_style::StyleArgs;
|
||||||
|
|
||||||
|
/// A Theme plugin that implement a style computation for a defined `WidgetStyle`
|
||||||
|
pub trait StyleProvider<S> {
|
||||||
|
/// The style according to the classes and state of the widget
|
||||||
|
fn style(&mut self, modifiers: &StyleArgs<'_>) -> S;
|
||||||
|
|
||||||
|
/// Used to tell different themes apart
|
||||||
|
fn type_id(&self) -> TypeId
|
||||||
|
where
|
||||||
|
Self: 'static,
|
||||||
|
{
|
||||||
|
TypeId::of::<Self>()
|
||||||
|
}
|
||||||
|
}
|
||||||
96
crates/egui/src/theme/themes.rs
Normal file
96
crates/egui/src/theme/themes.rs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use epaint::mutex::Mutex;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Id,
|
||||||
|
theme::{StyleProvider, default_style::DefaultStyle},
|
||||||
|
util::IdTypeMap,
|
||||||
|
widget_style::{
|
||||||
|
BaseStyle, ButtonStyle, CheckboxStyle, LabelStyle, SeparatorStyle, WidgetStyle,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The registry of [`StyleProvider`]s, one per [`WidgetStyle`] type.
|
||||||
|
///
|
||||||
|
/// Each widget asks this registry for the provider of its style type
|
||||||
|
/// (e.g. [`ButtonStyle`]), and that provider computes the final style from the
|
||||||
|
/// widget's classes and state.
|
||||||
|
///
|
||||||
|
/// A default provider is registered for every built-in style; register your
|
||||||
|
/// own with [`Context::add_widget_theme`](crate::Context::add_widget_theme) or
|
||||||
|
/// [`Context::replace_widget_theme`](crate::Context::replace_widget_theme).
|
||||||
|
pub struct Themes {
|
||||||
|
themes: IdTypeMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThemeWrap<S> = Arc<Mutex<Box<dyn StyleProvider<S> + Send + Sync>>>;
|
||||||
|
|
||||||
|
impl Default for Themes {
|
||||||
|
/// Register the default egui theme
|
||||||
|
fn default() -> Self {
|
||||||
|
let mut themes = IdTypeMap::default();
|
||||||
|
|
||||||
|
themes.insert_temp::<ThemeWrap<BaseStyle>>(
|
||||||
|
Id::NULL,
|
||||||
|
Arc::new(Mutex::new(Box::new(DefaultStyle))),
|
||||||
|
);
|
||||||
|
|
||||||
|
themes.insert_temp::<ThemeWrap<ButtonStyle>>(
|
||||||
|
Id::NULL,
|
||||||
|
Arc::new(Mutex::new(Box::new(DefaultStyle))),
|
||||||
|
);
|
||||||
|
|
||||||
|
themes.insert_temp::<ThemeWrap<SeparatorStyle>>(
|
||||||
|
Id::NULL,
|
||||||
|
Arc::new(Mutex::new(Box::new(DefaultStyle))),
|
||||||
|
);
|
||||||
|
|
||||||
|
themes.insert_temp::<ThemeWrap<CheckboxStyle>>(
|
||||||
|
Id::NULL,
|
||||||
|
Arc::new(Mutex::new(Box::new(DefaultStyle))),
|
||||||
|
);
|
||||||
|
|
||||||
|
themes.insert_temp::<ThemeWrap<LabelStyle>>(
|
||||||
|
Id::NULL,
|
||||||
|
Arc::new(Mutex::new(Box::new(DefaultStyle))),
|
||||||
|
);
|
||||||
|
|
||||||
|
Self { themes }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Themes {
|
||||||
|
/// Register a [`StyleProvider`] for the specified widget [`WidgetStyle`] `S`
|
||||||
|
///
|
||||||
|
/// Existing themes are overwritten if `force` is `true` or the new theme differs.
|
||||||
|
pub(crate) fn register<S: WidgetStyle + 'static>(
|
||||||
|
&mut self,
|
||||||
|
theme: impl StyleProvider<S> + Send + Sync + 'static,
|
||||||
|
force: bool,
|
||||||
|
) {
|
||||||
|
if !force
|
||||||
|
&& self
|
||||||
|
.themes
|
||||||
|
.get_temp::<ThemeWrap<S>>(Id::NULL)
|
||||||
|
.is_some_and(|t| t.lock().type_id() == theme.type_id())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.themes
|
||||||
|
.insert_temp::<ThemeWrap<S>>(Id::NULL, Arc::new(Mutex::new(Box::new(theme))));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch the style of the current theme
|
||||||
|
pub fn get<S: WidgetStyle + 'static>(&self) -> ThemeWrap<S> {
|
||||||
|
let v = self.themes.get_temp::<ThemeWrap<S>>(Id::NULL);
|
||||||
|
|
||||||
|
v.unwrap_or_else(|| {
|
||||||
|
panic!(
|
||||||
|
"A style should be set for {:?}",
|
||||||
|
core::any::type_name::<S>()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,318 +0,0 @@
|
|||||||
use std::{borrow::Cow, fmt};
|
|
||||||
|
|
||||||
use emath::Vec2;
|
|
||||||
use epaint::{Color32, FontId, Shadow, Stroke, text::TextWrapMode};
|
|
||||||
use smallvec::SmallVec;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
Frame, Response, Style, TextBuffer as _, TextStyle,
|
|
||||||
style::{WidgetVisuals, Widgets},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// General text style
|
|
||||||
pub struct TextVisuals {
|
|
||||||
/// Font used
|
|
||||||
pub font_id: FontId,
|
|
||||||
|
|
||||||
/// Font color
|
|
||||||
pub color: Color32,
|
|
||||||
|
|
||||||
/// Text decoration
|
|
||||||
pub underline: Stroke,
|
|
||||||
pub strikethrough: Stroke,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// General widget style
|
|
||||||
pub struct WidgetStyle {
|
|
||||||
pub frame: Frame,
|
|
||||||
|
|
||||||
pub text: TextVisuals,
|
|
||||||
|
|
||||||
pub stroke: Stroke,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Dedicated button style
|
|
||||||
pub struct ButtonStyle {
|
|
||||||
pub frame: Frame,
|
|
||||||
pub text_style: TextVisuals,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Dedicated checkbox style
|
|
||||||
pub struct CheckboxStyle {
|
|
||||||
/// Frame around
|
|
||||||
pub frame: Frame,
|
|
||||||
|
|
||||||
/// Text next to it
|
|
||||||
pub text_style: TextVisuals,
|
|
||||||
|
|
||||||
/// Checkbox size
|
|
||||||
pub checkbox_size: f32,
|
|
||||||
|
|
||||||
/// Checkmark size
|
|
||||||
pub check_size: f32,
|
|
||||||
|
|
||||||
/// Frame of the checkbox itself
|
|
||||||
pub checkbox_frame: Frame,
|
|
||||||
|
|
||||||
/// Checkmark stroke
|
|
||||||
pub check_stroke: Stroke,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Dedicated label style
|
|
||||||
pub struct LabelStyle {
|
|
||||||
/// Frame around
|
|
||||||
pub frame: Frame,
|
|
||||||
|
|
||||||
/// Text style
|
|
||||||
pub text: TextVisuals,
|
|
||||||
|
|
||||||
/// Wrap mode used
|
|
||||||
pub wrap_mode: TextWrapMode,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Dedicated separator style
|
|
||||||
pub struct SeparatorStyle {
|
|
||||||
/// How much space is allocated in the layout direction
|
|
||||||
pub spacing: f32,
|
|
||||||
|
|
||||||
/// How to paint it
|
|
||||||
pub stroke: Stroke,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The different state of a widget can be
|
|
||||||
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
|
|
||||||
pub enum WidgetState {
|
|
||||||
Noninteractive,
|
|
||||||
#[default]
|
|
||||||
Inactive,
|
|
||||||
Hovered,
|
|
||||||
Active,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widgets {
|
|
||||||
/// The widget visuals according to the state
|
|
||||||
pub fn state(&self, state: WidgetState) -> &WidgetVisuals {
|
|
||||||
match state {
|
|
||||||
WidgetState::Noninteractive => &self.noninteractive,
|
|
||||||
WidgetState::Inactive => &self.inactive,
|
|
||||||
WidgetState::Hovered => &self.hovered,
|
|
||||||
WidgetState::Active => &self.active,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Response {
|
|
||||||
pub fn widget_state(&self) -> WidgetState {
|
|
||||||
if !self.sense.interactive() {
|
|
||||||
WidgetState::Noninteractive
|
|
||||||
} else if self.is_pointer_button_down_on() || self.has_focus() || self.clicked() {
|
|
||||||
WidgetState::Active
|
|
||||||
} else if self.hovered() || self.highlighted() {
|
|
||||||
WidgetState::Hovered
|
|
||||||
} else {
|
|
||||||
WidgetState::Inactive
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Style {
|
|
||||||
/// The general widget style. The style is computed according to the classes and state of the widget.
|
|
||||||
pub fn widget_style(&self, _classes: &Classes, state: WidgetState) -> WidgetStyle {
|
|
||||||
let visuals = self.visuals.widgets.state(state);
|
|
||||||
let font_id = self.override_font_id.clone();
|
|
||||||
WidgetStyle {
|
|
||||||
frame: Frame {
|
|
||||||
fill: visuals.bg_fill,
|
|
||||||
stroke: visuals.bg_stroke,
|
|
||||||
corner_radius: visuals.corner_radius,
|
|
||||||
inner_margin: self.spacing.button_padding.into(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
stroke: visuals.fg_stroke,
|
|
||||||
text: TextVisuals {
|
|
||||||
color: self
|
|
||||||
.visuals
|
|
||||||
.override_text_color
|
|
||||||
.unwrap_or_else(|| visuals.text_color()),
|
|
||||||
font_id: font_id.unwrap_or_else(|| TextStyle::Body.resolve(self)),
|
|
||||||
strikethrough: Stroke::NONE,
|
|
||||||
underline: Stroke::NONE,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The dedicated button style. The style is computed according to the classes and state of the widget.
|
|
||||||
/// It depend on the general widget style.
|
|
||||||
pub fn button_style(&self, classes: &Classes, state: WidgetState) -> ButtonStyle {
|
|
||||||
let mut visuals = *self.visuals.widgets.state(state);
|
|
||||||
let mut ws = self.widget_style(classes, state);
|
|
||||||
|
|
||||||
if classes.has(SELECTED_CLASS) {
|
|
||||||
visuals.weak_bg_fill = self.visuals.selection.bg_fill;
|
|
||||||
visuals.bg_fill = self.visuals.selection.bg_fill;
|
|
||||||
visuals.fg_stroke = self.visuals.selection.stroke;
|
|
||||||
ws.text.color = self.visuals.selection.stroke.color;
|
|
||||||
}
|
|
||||||
|
|
||||||
ButtonStyle {
|
|
||||||
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: (self.spacing.button_padding + Vec2::splat(visuals.expansion)
|
|
||||||
- Vec2::splat(visuals.bg_stroke.width))
|
|
||||||
.into(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
text_style: ws.text,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The dedicated checkbox style. The style is computed according to the classes and state of the widget.
|
|
||||||
/// It depend on the general widget style.
|
|
||||||
pub fn checkbox_style(&self, classes: &Classes, state: WidgetState) -> CheckboxStyle {
|
|
||||||
let visuals = self.visuals.widgets.state(state);
|
|
||||||
let ws = self.widget_style(classes, state);
|
|
||||||
CheckboxStyle {
|
|
||||||
frame: Frame::new(),
|
|
||||||
checkbox_size: self.spacing.icon_width,
|
|
||||||
check_size: self.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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The dedicated label style. The style is computed according to the classes and state of the widget.
|
|
||||||
/// It depend on the general widget style.
|
|
||||||
pub fn label_style(&self, classes: &Classes, state: WidgetState) -> LabelStyle {
|
|
||||||
let ws = self.widget_style(classes, state);
|
|
||||||
LabelStyle {
|
|
||||||
frame: Frame {
|
|
||||||
fill: ws.frame.fill,
|
|
||||||
inner_margin: 0.0.into(),
|
|
||||||
outer_margin: 0.0.into(),
|
|
||||||
stroke: Stroke::NONE,
|
|
||||||
shadow: Shadow::NONE,
|
|
||||||
corner_radius: 0.into(),
|
|
||||||
},
|
|
||||||
text: ws.text,
|
|
||||||
wrap_mode: TextWrapMode::Wrap,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The dedicated separator style. The style is computed according to the classes and state of the widget.
|
|
||||||
/// It depend on the general widget style.
|
|
||||||
pub fn separator_style(&self, _classes: &Classes, _state: WidgetState) -> SeparatorStyle {
|
|
||||||
let visuals = self.visuals.noninteractive();
|
|
||||||
SeparatorStyle {
|
|
||||||
spacing: 6.0,
|
|
||||||
stroke: visuals.bg_stroke,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The root class is a special class present on every top-level [`crate::Ui`].
|
|
||||||
pub const ROOT_CLASS: &str = "root";
|
|
||||||
|
|
||||||
/// The selected class is a special class present on selected [`crate::Button`].
|
|
||||||
pub const SELECTED_CLASS: &str = "selected";
|
|
||||||
|
|
||||||
/// A class is a static string identifier.
|
|
||||||
pub type ClassName = Cow<'static, str>;
|
|
||||||
|
|
||||||
/// Classes are string identifier that can be set on widget/Ui.
|
|
||||||
///
|
|
||||||
/// This can be used by styling engine to compute a different style
|
|
||||||
/// based on the set of classes present on the widget/Ui.
|
|
||||||
#[derive(Debug, Default, Clone)]
|
|
||||||
pub struct Classes {
|
|
||||||
classes: SmallVec<[ClassName; 5]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Classes {
|
|
||||||
/// Add a class to the list if the condition is true
|
|
||||||
#[inline]
|
|
||||||
fn add_if(&mut self, class: impl Into<ClassName>, condition: bool) {
|
|
||||||
if condition {
|
|
||||||
self.classes.push(class.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HasClasses for Classes {
|
|
||||||
fn classes(&self) -> &Classes {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn classes_mut(&mut self) -> &mut Classes {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl core::fmt::Display for Classes {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
self.classes.iter().for_each(|class| {
|
|
||||||
let _ = f.write_str(class.as_str());
|
|
||||||
});
|
|
||||||
f.write_str("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Any widgets supporting [`Classes`] must implement this trait
|
|
||||||
pub trait HasClasses {
|
|
||||||
fn classes(&self) -> &Classes;
|
|
||||||
|
|
||||||
fn classes_mut(&mut self) -> &mut Classes;
|
|
||||||
|
|
||||||
/// Add the given class by consuming [`self`]
|
|
||||||
#[inline]
|
|
||||||
fn with_class(mut self, class: impl Into<ClassName>) -> Self
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
self.classes_mut().add_if(class.into(), true);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add the given class by consuming [`self`] if the condition is true
|
|
||||||
#[inline]
|
|
||||||
fn with_class_if(mut self, class: impl Into<ClassName>, condition: bool) -> Self
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
self.classes_mut().add_if(class.into(), condition);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add the given class in-place
|
|
||||||
#[inline]
|
|
||||||
fn add_class(&mut self, class: impl Into<ClassName>) -> &mut Self
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
self.classes_mut().add_if(class.into(), true);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add the given class in-place if the condition is true
|
|
||||||
#[inline]
|
|
||||||
fn add_class_if(&mut self, class: impl Into<ClassName>, condition: bool) -> &mut Self
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
self.classes_mut().add_if(class.into(), condition);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// True if the class is present
|
|
||||||
fn has(&self, class: impl Into<ClassName>) -> bool {
|
|
||||||
self.classes().classes.contains(&class.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
109
crates/egui/src/widget_style/classes.rs
Normal file
109
crates/egui/src/widget_style/classes.rs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
use std::{borrow::Cow, fmt};
|
||||||
|
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
use crate::TextBuffer as _;
|
||||||
|
|
||||||
|
/// The root class is a special class present on every top-level [`crate::Ui`].
|
||||||
|
pub const ROOT_CLASS: &str = "root";
|
||||||
|
|
||||||
|
/// The selected class is a special class present on selected [`crate::Button`].
|
||||||
|
pub const SELECTED_CLASS: &str = "selected";
|
||||||
|
|
||||||
|
/// A class is a static string identifier.
|
||||||
|
pub type ClassName = Cow<'static, str>;
|
||||||
|
|
||||||
|
/// Classes are string identifier that can be set on widget/Ui.
|
||||||
|
///
|
||||||
|
/// This can be used by styling engine to compute a different style
|
||||||
|
/// based on the set of classes present on the widget/Ui.
|
||||||
|
#[derive(Debug, Default, Clone, Hash)]
|
||||||
|
pub struct Classes {
|
||||||
|
classes: SmallVec<[ClassName; 5]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Classes {
|
||||||
|
/// Add a class to the list if the condition is true
|
||||||
|
#[inline]
|
||||||
|
fn add_if(&mut self, class: impl Into<ClassName>, condition: bool) {
|
||||||
|
if condition {
|
||||||
|
self.classes.push(class.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasClasses for Classes {
|
||||||
|
fn classes(&self) -> &Classes {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn classes_mut(&mut self) -> &mut Classes {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Display for Classes {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
self.classes.iter().for_each(|class| {
|
||||||
|
let _ = f.write_str(class.as_str());
|
||||||
|
});
|
||||||
|
f.write_str("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Any widgets supporting [`Classes`] must implement this trait
|
||||||
|
pub trait HasClasses {
|
||||||
|
fn classes(&self) -> &Classes;
|
||||||
|
|
||||||
|
fn classes_mut(&mut self) -> &mut Classes;
|
||||||
|
|
||||||
|
/// Add the given class by consuming `self`
|
||||||
|
#[inline]
|
||||||
|
fn with_class(mut self, class: impl Into<ClassName>) -> Self
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
self.classes_mut().add_if(class.into(), true);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add the given class by consuming `self` if the condition is true
|
||||||
|
#[inline]
|
||||||
|
fn with_class_if(mut self, class: impl Into<ClassName>, condition: bool) -> Self
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
self.classes_mut().add_if(class.into(), condition);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add the given class in-place
|
||||||
|
#[inline]
|
||||||
|
fn add_class(&mut self, class: impl Into<ClassName>) -> &mut Self
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
self.classes_mut().add_if(class.into(), true);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add the given class in-place if the condition is true
|
||||||
|
#[inline]
|
||||||
|
fn add_class_if(&mut self, class: impl Into<ClassName>, condition: bool) -> &mut Self
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
self.classes_mut().add_if(class.into(), condition);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True if the class is present
|
||||||
|
fn has(&self, class: impl Into<ClassName>) -> bool {
|
||||||
|
self.classes().classes.contains(&class.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of class
|
||||||
|
fn as_slice(&self) -> &[ClassName] {
|
||||||
|
&self.classes().classes
|
||||||
|
}
|
||||||
|
}
|
||||||
149
crates/egui/src/widget_style/mod.rs
Normal file
149
crates/egui/src/widget_style/mod.rs
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
// This module is only public with the `experimental_theme` feature,
|
||||||
|
// so without it a lot of it looks unused:
|
||||||
|
#![cfg_attr(not(feature = "experimental"), allow(dead_code, unused_imports))]
|
||||||
|
|
||||||
|
mod classes;
|
||||||
|
|
||||||
|
pub use self::classes::{ClassName, Classes, HasClasses, ROOT_CLASS, SELECTED_CLASS};
|
||||||
|
|
||||||
|
use core::fmt::Debug;
|
||||||
|
|
||||||
|
use epaint::{Color32, FontId, Stroke, text::TextWrapMode};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Context, Frame, Response, Style, UiStack,
|
||||||
|
style::{WidgetVisuals, Widgets},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Each dedicated style must implement this trait to be used in the theme plugin system
|
||||||
|
pub trait WidgetStyle: Debug + Clone + Send + Sync + core::any::Any + 'static {}
|
||||||
|
|
||||||
|
/// General text style
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TextVisuals {
|
||||||
|
/// Font used
|
||||||
|
pub font_id: FontId,
|
||||||
|
|
||||||
|
/// Font color
|
||||||
|
pub color: Color32,
|
||||||
|
|
||||||
|
/// Text decoration
|
||||||
|
pub underline: Stroke,
|
||||||
|
pub strikethrough: Stroke,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// General widget style
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BaseStyle {
|
||||||
|
pub frame: Frame,
|
||||||
|
|
||||||
|
pub text: TextVisuals,
|
||||||
|
|
||||||
|
pub stroke: Stroke,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetStyle for BaseStyle {}
|
||||||
|
|
||||||
|
/// Dedicated button style
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ButtonStyle {
|
||||||
|
pub frame: Frame,
|
||||||
|
pub text_style: TextVisuals,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetStyle for ButtonStyle {}
|
||||||
|
|
||||||
|
/// Dedicated checkbox style
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CheckboxStyle {
|
||||||
|
/// Frame around
|
||||||
|
pub frame: Frame,
|
||||||
|
|
||||||
|
/// Text next to it
|
||||||
|
pub text_style: TextVisuals,
|
||||||
|
|
||||||
|
/// Checkbox size
|
||||||
|
pub checkbox_size: f32,
|
||||||
|
|
||||||
|
/// Checkmark size
|
||||||
|
pub check_size: f32,
|
||||||
|
|
||||||
|
/// Frame of the checkbox itself
|
||||||
|
pub checkbox_frame: Frame,
|
||||||
|
|
||||||
|
/// Checkmark stroke
|
||||||
|
pub check_stroke: Stroke,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetStyle for CheckboxStyle {}
|
||||||
|
|
||||||
|
/// Dedicated label style
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct LabelStyle {
|
||||||
|
/// Frame around
|
||||||
|
pub frame: Frame,
|
||||||
|
|
||||||
|
/// Text style
|
||||||
|
pub text: TextVisuals,
|
||||||
|
|
||||||
|
/// Wrap mode used
|
||||||
|
pub wrap_mode: TextWrapMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetStyle for LabelStyle {}
|
||||||
|
|
||||||
|
/// Dedicated separator style
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SeparatorStyle {
|
||||||
|
/// How much space is allocated in the layout direction
|
||||||
|
pub spacing: f32,
|
||||||
|
|
||||||
|
/// How to paint it
|
||||||
|
pub stroke: Stroke,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetStyle for SeparatorStyle {}
|
||||||
|
|
||||||
|
/// The different state of a widget can be
|
||||||
|
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub enum WidgetState {
|
||||||
|
Noninteractive,
|
||||||
|
#[default]
|
||||||
|
Inactive,
|
||||||
|
Hovered,
|
||||||
|
Active,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widgets {
|
||||||
|
/// The widget visuals according to the state
|
||||||
|
pub fn state(&self, state: WidgetState) -> &WidgetVisuals {
|
||||||
|
match state {
|
||||||
|
WidgetState::Noninteractive => &self.noninteractive,
|
||||||
|
WidgetState::Inactive => &self.inactive,
|
||||||
|
WidgetState::Hovered => &self.hovered,
|
||||||
|
WidgetState::Active => &self.active,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
pub fn widget_state(&self) -> WidgetState {
|
||||||
|
if !self.sense.interactive() {
|
||||||
|
WidgetState::Noninteractive
|
||||||
|
} else if self.is_pointer_button_down_on() || self.has_focus() || self.clicked() {
|
||||||
|
WidgetState::Active
|
||||||
|
} else if self.hovered() || self.highlighted() {
|
||||||
|
WidgetState::Hovered
|
||||||
|
} else {
|
||||||
|
WidgetState::Inactive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StyleArgs<'a> {
|
||||||
|
pub classes: &'a Classes,
|
||||||
|
pub state: WidgetState,
|
||||||
|
pub stack: &'a UiStack,
|
||||||
|
pub style: &'a Style,
|
||||||
|
pub ctx: &'a Context,
|
||||||
|
}
|
||||||
@@ -328,7 +328,7 @@ 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.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
|
||||||
|
|||||||
@@ -70,9 +70,6 @@ impl Widget for Checkbox<'_> {
|
|||||||
|
|
||||||
// Get the widget style by reading the response from the previous pass
|
// Get the widget style by reading the response from the previous pass
|
||||||
let id = ui.next_auto_id();
|
let id = ui.next_auto_id();
|
||||||
let response: Option<Response> = ui.ctx().read_response(id);
|
|
||||||
let state = response.map(|r| r.widget_state()).unwrap_or_default();
|
|
||||||
|
|
||||||
let CheckboxStyle {
|
let CheckboxStyle {
|
||||||
check_size,
|
check_size,
|
||||||
checkbox_frame,
|
checkbox_frame,
|
||||||
@@ -80,7 +77,7 @@ impl Widget for Checkbox<'_> {
|
|||||||
frame,
|
frame,
|
||||||
check_stroke,
|
check_stroke,
|
||||||
text_style,
|
text_style,
|
||||||
} = ui.style().checkbox_style(&classes, state);
|
} = ui.widget_style(id, &classes);
|
||||||
|
|
||||||
let mut min_size = Vec2::splat(ui.spacing().interact_size.y);
|
let mut min_size = Vec2::splat(ui.spacing().interact_size.y);
|
||||||
min_size.y = min_size.y.at_least(checkbox_size);
|
min_size.y = min_size.y.at_least(checkbox_size);
|
||||||
|
|||||||
@@ -101,12 +101,10 @@ impl Widget for Separator {
|
|||||||
|
|
||||||
// Get the widget style by reading the response from the previous pass
|
// Get the widget style by reading the response from the previous pass
|
||||||
let id = ui.next_auto_id();
|
let id = ui.next_auto_id();
|
||||||
let response: Option<Response> = ui.ctx().read_response(id);
|
|
||||||
let state = response.map(|r| r.widget_state()).unwrap_or_default();
|
|
||||||
let SeparatorStyle {
|
let SeparatorStyle {
|
||||||
spacing: spacing_style,
|
spacing: spacing_style,
|
||||||
stroke,
|
stroke,
|
||||||
} = ui.style().separator_style(&classes, state);
|
} = ui.widget_style(id, &classes);
|
||||||
|
|
||||||
// override the spacing if not set
|
// override the spacing if not set
|
||||||
let spacing = spacing.unwrap_or(spacing_style);
|
let spacing = spacing.unwrap_or(spacing_style);
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:05527c073b1ee2f6a15052a9097d1ad515331ff32d572cf510086f9ebb7a7bbc
|
oid sha256:ad431823e85de056c95e6e97d1a9f5905880a1ecc34f24da5bc0b611e42fe99b
|
||||||
size 27253
|
size 27260
|
||||||
|
|||||||
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
|
||||||
|
"experimental", # this example is all about the experimental theming API
|
||||||
|
] }
|
||||||
|
env_logger = { workspace = true, features = ["auto-color", "humantime"] }
|
||||||
10
examples/styling_engine/README.md
Normal file
10
examples/styling_engine/README.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Example showing how the style engine work.
|
||||||
|
|
||||||
|
A custom `StyleProvider<ButtonStyle>` 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
|
||||||
|
```
|
||||||
|
|
||||||
|
<!--  -->
|
||||||
162
examples/styling_engine/src/main.rs
Normal file
162
examples/styling_engine/src/main.rs
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||||
|
|
||||||
|
//! 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, CentralPanel, Color32, Frame, Panel,
|
||||||
|
theme::StyleProvider,
|
||||||
|
widget_style::{BaseStyle, ButtonStyle, HasClasses as _, StyleArgs, WidgetState},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Buttons with this class are styled as a destructive action.
|
||||||
|
const DANGER: &str = "danger";
|
||||||
|
|
||||||
|
/// 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<ButtonStyle> 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, 340.0]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut preset = Preset::Ocean;
|
||||||
|
let mut theme = MyTheme::preset(preset);
|
||||||
|
let mut last_click = "nothing";
|
||||||
|
|
||||||
|
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.
|
||||||
|
ui.add_widget_theme::<ButtonStyle>(theme);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
ui.heading("Button theme");
|
||||||
|
|
||||||
|
let mut changed = false;
|
||||||
|
|
||||||
|
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.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::<ButtonStyle>(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(egui::Button::new("Delete everything").with_class(DANGER))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
last_click = "Delete everything";
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add_space(8.0);
|
||||||
|
ui.label(format!("Last clicked: {last_click}"));
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ version.workspace = true
|
|||||||
ignored = ["image"] # We need the png feature
|
ignored = ["image"] # We need the png feature
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
egui = { workspace = true, default-features = true }
|
egui = { workspace = true, default-features = true, features = ["experimental"] }
|
||||||
egui_kittest = { workspace = true, features = ["snapshot", "wgpu"] }
|
egui_kittest = { workspace = true, features = ["snapshot", "wgpu"] }
|
||||||
egui_extras = { workspace = true, features = ["image"] }
|
egui_extras = { workspace = true, features = ["image"] }
|
||||||
image = { workspace = true, features = ["png"] }
|
image = { workspace = true, features = ["png"] }
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use egui::{
|
use egui::{
|
||||||
Align, Atom, AtomExt as _, AtomLayout, Button, Direction, Frame, Layout, TextWrapMode, Ui, Vec2,
|
Align, Atom, AtomExt as _, AtomLayout, Button, Direction, Frame, Layout, TextWrapMode, Ui,
|
||||||
|
Vec2,
|
||||||
|
widget_style::{ButtonStyle, StyleArgs},
|
||||||
};
|
};
|
||||||
use egui_kittest::{HarnessBuilder, SnapshotResult, SnapshotResults};
|
use egui_kittest::{HarnessBuilder, SnapshotResult, SnapshotResults};
|
||||||
|
|
||||||
@@ -129,11 +131,14 @@ fn test_atom_layout_nesting_and_direction() {
|
|||||||
let style = ui.style();
|
let style = ui.style();
|
||||||
let canvas_frame = Frame::canvas(style);
|
let canvas_frame = Frame::canvas(style);
|
||||||
|
|
||||||
let button_frame = style
|
let button_frame = ui
|
||||||
.button_style(
|
.get_widget_style::<ButtonStyle>(&StyleArgs {
|
||||||
&egui::widget_style::Classes::default(),
|
classes: &egui::widget_style::Classes::default(),
|
||||||
egui::widget_style::WidgetState::Inactive,
|
state: egui::widget_style::WidgetState::Inactive,
|
||||||
)
|
ctx: ui,
|
||||||
|
stack: ui.stack(),
|
||||||
|
style,
|
||||||
|
})
|
||||||
.frame;
|
.frame;
|
||||||
|
|
||||||
let row = |direction: Direction| {
|
let row = |direction: Direction| {
|
||||||
|
|||||||
Reference in New Issue
Block a user