1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-01 14:20:04 -04:00

Rename classes, more button options via StyleProvider (#8464)

Moves global classnames to e.g. `egui::class::ROOT`.

Widget specific classes live on the widgets struct. 

Adds some more classes for our buttons styling options (`CLASS_SMALL`
etc), so that custom `StyleProvider`s can choose to implement them
differently or ignore them altogether.


Also removes some styleproviders that were partly or completely unused
(e.g. basestyle / labelstyle). We can add them back once we actually
implement styling for those.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lucas Meurer
2026-09-01 11:39:13 +02:00
committed by GitHub
parent 080d512f45
commit de077b92d3
22 changed files with 631 additions and 359 deletions

View File

@@ -31,7 +31,6 @@ pub enum AtomKind<'a> {
/// Text atom.
///
/// Truncation within [`crate::AtomLayout`] works like this:
/// -
/// - if `wrap_mode` is not Extend
/// - if no atom is `shrink`
/// - the first text atom is selected and will be marked as `shrink`

View File

@@ -0,0 +1,147 @@
use core::borrow::Borrow;
use std::borrow::Cow;
use std::fmt;
use std::sync::Arc;
/// A class, used to customize widget styling and behavior.
#[derive(Clone)]
pub struct ClassName(ClassNameInner);
#[derive(Clone)]
enum ClassNameInner {
Static(&'static str),
Owned(Arc<str>),
}
impl ClassName {
/// A class from a string known at compile time. This never allocates.
#[inline]
pub const fn from_static(class: &'static str) -> Self {
Self(ClassNameInner::Static(class))
}
/// A class from anything that converts into one.
#[inline]
pub fn new(class: impl Into<Self>) -> Self {
class.into()
}
/// The class as a string.
#[inline]
pub fn as_str(&self) -> &str {
match &self.0 {
ClassNameInner::Static(class) => class,
ClassNameInner::Owned(class) => class,
}
}
}
impl From<&'static str> for ClassName {
#[inline]
fn from(class: &'static str) -> Self {
Self::from_static(class)
}
}
impl From<String> for ClassName {
#[inline]
fn from(class: String) -> Self {
Self(ClassNameInner::Owned(class.into()))
}
}
impl From<&String> for ClassName {
#[inline]
fn from(class: &String) -> Self {
Self(ClassNameInner::Owned(class.as_str().into()))
}
}
impl From<Arc<str>> for ClassName {
#[inline]
fn from(class: Arc<str>) -> Self {
Self(ClassNameInner::Owned(class))
}
}
impl From<Cow<'static, str>> for ClassName {
#[inline]
fn from(class: Cow<'static, str>) -> Self {
match class {
Cow::Borrowed(class) => Self::from_static(class),
Cow::Owned(class) => class.into(),
}
}
}
impl From<&Self> for ClassName {
#[inline]
fn from(class: &Self) -> Self {
class.clone()
}
}
impl Borrow<str> for ClassName {
#[inline]
fn borrow(&self) -> &str {
self.as_str()
}
}
impl AsRef<str> for ClassName {
#[inline]
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl core::ops::Deref for ClassName {
type Target = str;
#[inline]
fn deref(&self) -> &str {
self.as_str()
}
}
impl PartialEq for ClassName {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.as_str() == other.as_str()
}
}
impl Eq for ClassName {}
impl PartialEq<str> for ClassName {
#[inline]
fn eq(&self, other: &str) -> bool {
self.as_str() == other
}
}
impl PartialEq<&str> for ClassName {
#[inline]
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl core::hash::Hash for ClassName {
#[inline]
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.as_str().hash(state);
}
}
impl fmt::Debug for ClassName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.as_str().fmt(f)
}
}
impl fmt::Display for ClassName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}

View File

@@ -0,0 +1,104 @@
use crate::class::{ClassName, HasClasses};
use smallvec::SmallVec;
use std::fmt;
/// [`Classes`] is a collection of [`ClassName`]s that can be added to widgets or containers.
///
/// This can be used by styling engine to compute a different style
/// based on the set of classes present on the widget/Ui.
/// You could also use it to e.g. change widget behavior based on the context of some container.
///
/// Class order is preserved and may be used by a style provider for precedence (last class should win).
///
/// Use [`HasClasses`] to add/modify classes.
#[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.
///
/// A class is never added twice. This never removes a class: use [`Self::set`] for that.
#[inline]
pub(crate) fn add_if(&mut self, class: impl Into<ClassName>, condition: bool) {
if condition {
self.set(class, true);
}
}
/// Add the class if `present`, remove it otherwise.
#[inline]
pub(crate) fn set(&mut self, class: impl Into<ClassName>, present: bool) {
let class = class.into();
// Always retain and push again, since order of classes can matter.
self.classes.retain(|existing| existing != &class);
if present {
self.classes.push(class);
}
}
/// Extend the classes and deduplicate them.
///
/// A class that is already present is moved to the end, since order can matter.
#[inline]
pub(crate) fn extend(&mut self, classes: impl IntoIterator<Item = impl Into<ClassName>>) {
for class in classes {
self.set(class, true);
}
}
/// Iterate over the classes, in order.
#[inline]
pub fn iter(&self) -> core::slice::Iter<'_, ClassName> {
self.classes.iter()
}
/// Return the classes as a slice
#[inline]
pub fn as_slice(&self) -> &[ClassName] {
self.classes.as_slice()
}
}
impl IntoIterator for Classes {
type Item = ClassName;
type IntoIter = smallvec::IntoIter<[ClassName; 5]>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
self.classes.into_iter()
}
}
impl<'a> IntoIterator for &'a Classes {
type Item = &'a ClassName;
type IntoIter = core::slice::Iter<'a, ClassName>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
self.classes.iter()
}
}
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 {
for (i, class) in self.classes.iter().enumerate() {
if i > 0 {
f.write_str(" ")?;
}
f.write_str(class.as_str())?;
}
Ok(())
}
}

View File

@@ -0,0 +1,140 @@
use crate::class::ClassName;
/// Any widgets supporting [`crate::class::Classes`] must implement this trait.
pub trait HasClasses {
fn classes(&self) -> &crate::class::Classes;
fn classes_mut(&mut self) -> &mut crate::class::Classes;
/// True if the class is present.
#[inline]
fn has_class(&self, class: &str) -> bool {
self.classes()
.iter()
.any(|existing| existing.as_str() == class)
}
/// 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 `present`, remove it otherwise.
///
/// Use this for a setter that takes a `bool`, so that the last call wins.
#[inline]
fn set_class(&mut self, class: impl Into<ClassName>, present: bool) -> &mut Self
where
Self: Sized,
{
self.classes_mut().set(class.into(), present);
self
}
/// Remove the given class in-place.
#[inline]
fn remove_class(&mut self, class: impl Into<ClassName>) -> &mut Self
where
Self: Sized,
{
self.classes_mut().set(class.into(), false);
self
}
/// 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
}
/// Append all the given classes at the end, deduplicating them.
#[inline]
fn add_classes(&mut self, classes: crate::class::Classes) -> &mut Self {
self.classes_mut().extend(classes);
self
}
/// Append all the given classes at the end, deduplicating them. Consuming `self`.
#[inline]
fn with_classes(mut self, classes: crate::class::Classes) -> Self
where
Self: Sized,
{
self.classes_mut().extend(classes);
self
}
/// Add a class to the list if the condition is true.
///
/// A class is never added twice. This never removes a class: use [`Self::set_class`] for that.
#[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
}
/// Add the given class by consuming `self` if the condition is true.
///
/// A class is never added twice. This never removes a class: use [`Self::set_class`] for that.
#[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
}
/// Iterate over the classes, in the order they were added.
#[inline]
fn iter_classes(&self) -> core::slice::Iter<'_, ClassName> {
self.classes().iter()
}
/// Return the classes as a slice.
#[inline]
fn classes_as_slice(&self) -> &[ClassName] {
self.classes().as_slice()
}
}
#[cfg(test)]
mod tests {
use crate::class::{Classes, HasClasses as _};
#[test]
fn setting_a_class_moves_it_to_end() {
let mut classes = Classes::default();
classes.add_class("first");
classes.add_class("updated");
classes.add_class("second");
classes.set_class("updated", true);
assert_eq!(classes.as_slice(), ["first", "second", "updated"]);
}
#[test]
fn adding_a_class_twice_moves_it_to_end() {
let mut classes = Classes::default();
classes.add_class("first");
classes.add_class("updated");
classes.add_class("second");
classes.add_class("updated");
assert_eq!(classes.as_slice(), ["first", "second", "updated"]);
}
}

View File

@@ -0,0 +1,10 @@
mod class_name;
mod classes;
mod has_classes;
pub use class_name::ClassName;
pub use classes::Classes;
pub use has_classes::HasClasses;
/// Present on every top-level [`crate::Ui`].
pub const ROOT: ClassName = ClassName::from_static("egui::root");

View File

@@ -298,6 +298,25 @@ impl Frame {
self
}
/// Handle `stroke` and `expansion` without affecting layout.
///
/// This handles `expansion` by subtracting it from the outer margin and adding it to the
/// inner margin. It also corrects for `stroke`, by subtracting the stroke width from `inner_margin`.
///
/// Use this when stroke or expansion might change on hover, and you don't want it to cause
/// layout shifts.
#[inline]
pub fn apply_stroke_and_expansion_without_layout_shift(
mut self,
stroke: Stroke,
expansion: f32,
) -> Self {
self.outer_margin = self.outer_margin - Margin::from(expansion);
self.inner_margin = self.inner_margin + Margin::from(expansion - stroke.width);
self.stroke = stroke;
self
}
/// Optional drop-shadow behind the frame.
#[inline]
pub fn shadow(mut self, shadow: Shadow) -> Self {
@@ -316,6 +335,17 @@ impl Frame {
self.shadow.color = self.shadow.color.gamma_multiply(opacity);
self
}
/// Make this frame invisible by setting background and stroke to transparent.
///
/// Will not affect layout or contents.
#[inline]
pub fn invisible(mut self) -> Self {
self.fill = Color32::TRANSPARENT;
self.stroke.color = Color32::TRANSPARENT;
self.shadow = Shadow::NONE;
self
}
}
/// ## Inspectors

View File

@@ -753,6 +753,9 @@ impl Default for Context {
ctx.add_plugin(crate::text_selection::LabelSelectionState::default());
ctx.add_plugin(crate::DragAndDrop::default());
// Register the default theme for all built-in widgets:
theme::DefaultStyle::register(&ctx);
ctx
}
}
@@ -2104,7 +2107,10 @@ impl Context {
/// 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")]
///
/// The types you need to call this (e.g. `StyleProvider`) are only public
/// with the `experimental_theme` feature.
#[cfg_attr(not(feature = "experimental"), doc(hidden))]
pub fn add_widget_theme<S: WidgetStyle + 'static>(
&self,
theme: impl theme::StyleProvider<S> + Send + Sync + 'static,
@@ -2116,7 +2122,10 @@ impl Context {
///
/// Overwrite any theme already registered for the specified widget [`WidgetStyle`].
/// This allow to live edit a theme.
#[cfg(feature = "experimental")]
///
/// The types you need to call this (e.g. `StyleProvider`) are only public
/// with the `experimental_theme` feature.
#[cfg_attr(not(feature = "experimental"), doc(hidden))]
pub fn replace_widget_theme<S: WidgetStyle + 'static>(
&self,
theme: impl theme::StyleProvider<S> + Send + Sync + 'static,

View File

@@ -437,6 +437,7 @@ pub mod widgets;
#[cfg(feature = "callstack")]
#[cfg(debug_assertions)]
mod callstack;
pub mod class;
pub use accesskit;

View File

@@ -1,12 +1,12 @@
use emath::Vec2;
use epaint::{Shadow, Stroke, text::TextWrapMode};
use epaint::Margin;
use crate::{
Frame, TextStyle,
Button, Context, Frame, TextStyle,
class::HasClasses as _,
theme::StyleProvider,
widget_style::{
BaseStyle, ButtonStyle, CheckboxStyle, HasClasses as _, LabelStyle, SELECTED_CLASS,
SeparatorStyle, StyleArgs, TextVisuals, WidgetState,
ButtonStyle, CheckboxStyle, SeparatorStyle, StyleArgs, TextVisuals, WidgetState,
},
};
@@ -15,97 +15,88 @@ use crate::{
#[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 DefaultStyle {
/// Register `Self` as the [`StyleProvider`] of every built-in widget style.
///
/// [`Context::default`] does this. Any theme you register yourself
/// replaces the default one for that widget style.
pub fn register(ctx: &Context) {
ctx.add_widget_theme::<ButtonStyle>(Self);
ctx.add_widget_theme::<SeparatorStyle>(Self);
ctx.add_widget_theme::<CheckboxStyle>(Self);
}
}
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 widget_visuals = *style.visuals.widgets.state(*state);
let mut ws: BaseStyle = ctx.get_widget_style(modifiers);
if classes.has(SELECTED_CLASS) {
if classes.has_class(&Button::CLASS_SELECTED) {
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;
}
let mut inner_margin: Margin = spacing.button_padding.into();
// A small button as high as regular text
if classes.has_class(&Button::CLASS_SMALL) {
inner_margin.top = 0;
inner_margin.bottom = 0;
}
let painted_frame = Frame {
fill: widget_visuals.weak_bg_fill,
corner_radius: widget_visuals.corner_radius,
inner_margin,
..Default::default()
}
// Ensure changing expansion and stroke don't affect layout:
.apply_stroke_and_expansion_without_layout_shift(
widget_visuals.bg_stroke,
widget_visuals.expansion,
);
let has_frame = classes.has_class(&Button::CLASS_FRAME)
|| (!classes.has_class(&Button::CLASS_NO_FRAME) && style.visuals.button_frame);
let frame = if !has_frame {
// No frame at all: the button takes up no more room than its contents.
Frame::new()
} else if classes.has_class(&Button::CLASS_HIDE_FRAME_WHEN_INACTIVE)
&& *state == WidgetState::Inactive
{
// Hide the frame, but keep its spacing
painted_frame.invisible()
} else {
painted_frame
};
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()
min_size: if classes.has_class(&Button::CLASS_SMALL) {
Vec2::ZERO
} else {
Vec2::new(0.0, spacing.interact_size.y)
},
text_style: ws.text,
frame,
text_style: TextVisuals::from_widget_visuals(style, TextStyle::Body, &widget_visuals),
}
}
}
impl StyleProvider<CheckboxStyle> for DefaultStyle {
fn style(&mut self, modifiers: &StyleArgs<'_>) -> CheckboxStyle {
let StyleArgs {
ctx, style, state, ..
} = modifiers;
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,
};
let ws: BaseStyle = ctx.get_widget_style(modifiers);
let widget_visuals = *style.visuals.widgets.state(*state);
CheckboxStyle {
frame: Frame::new(),
@@ -117,28 +108,8 @@ impl StyleProvider<CheckboxStyle> for DefaultStyle {
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,
text_style: TextVisuals::from_widget_visuals(style, TextStyle::Body, &widget_visuals),
check_stroke: widget_visuals.fg_stroke,
}
}
}

View File

@@ -12,7 +12,8 @@ pub use self::{default_style::DefaultStyle, style_provider::StyleProvider, theme
use crate::{
Ui,
widget_style::{Classes, StyleArgs, WidgetState, WidgetStyle},
class::Classes,
widget_style::{StyleArgs, WidgetState, WidgetStyle},
};
impl Ui {

View File

@@ -2,64 +2,25 @@ 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,
},
};
use crate::{Id, theme::StyleProvider, util::IdTypeMap, widget_style::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
/// (e.g. [`crate::widget_style::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).
/// A default provider is registered for every built-in style. Register your
/// own with [`crate::Context::add_widget_theme`] or [`crate::Context::replace_widget_theme`].
///
/// The [`crate::theme::DefaultStyle`] is registered in [`crate::Context::default`].
#[derive(Default)]
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`
///

View File

@@ -5,8 +5,8 @@ use core::{any::Any, ops::Deref};
use std::sync::Arc;
use crate::containers::menu;
use crate::widget_style::{HasClasses as _, ROOT_CLASS};
use crate::{IdSource, containers::*, ecolor::*, layout::*, placer::Placer, widgets::*, *};
use crate::{class, class::HasClasses as _};
use emath::GuiRounding as _;
// ----------------------------------------------------------------------------
@@ -135,7 +135,7 @@ impl Ui {
let disabled = disabled || invisible;
let style = style.unwrap_or_else(|| ctx.global_style());
let sense = sense.unwrap_or_else(Sense::hover);
let classes = classes.with_class(ROOT_CLASS);
let classes = classes.with_class(class::ROOT);
let placer = Placer::new(max_rect, layout);
let ui_stack = UiStack {

View File

@@ -4,7 +4,7 @@ use std::sync::Arc;
use crate::Ui;
use crate::{
AsIdSalt, ClosableTag, Id, IdSalt, LayerId, Layout, Rect, Sense, Style, UiStackInfo,
widget_style::{Classes, HasClasses},
class::{Classes, HasClasses},
};
/// The properties specified when creating a top-level or child [`Ui`].

View File

@@ -1,7 +1,7 @@
use core::{any::Any, iter::FusedIterator};
use std::sync::Arc;
use crate::widget_style::Classes;
use crate::class::{Classes, HasClasses as _};
use epaint::Color32;
use crate::{Direction, Frame, Id, Rect};
@@ -290,6 +290,11 @@ impl UiStack {
pub fn contained_in(&self, kind: UiKind) -> bool {
self.iter().any(|frame| frame.kind() == Some(kind))
}
/// Does this node, or any [`crate::Ui`] up the stack, carry this class?
pub fn has_class(&self, class: &str) -> bool {
self.iter().any(|node| node.classes.has_class(class))
}
}
// ----------------------------------------------------------------------------

View File

@@ -1,109 +0,0 @@
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
}
}

View File

@@ -2,16 +2,12 @@
// 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 epaint::{Color32, FontId, Stroke, Vec2};
use crate::{
Context, Frame, Response, Style, UiStack,
Context, FontSelection, Frame, Response, Style, UiStack,
class::{Classes, HasClasses as _},
style::{WidgetVisuals, Widgets},
};
@@ -26,27 +22,38 @@ pub struct TextVisuals {
/// 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,
impl TextVisuals {
/// Text in `color`, using the given font.
///
/// `style.override_font_id` wins over `font`, if it is set.
pub fn new(style: &Style, font: impl Into<FontSelection>, color: Color32) -> Self {
Self {
color,
font_id: style
.override_font_id
.clone()
.unwrap_or_else(|| font.into().resolve(style)),
}
}
pub text: TextVisuals,
pub stroke: Stroke,
/// The text of a widget, colored by the [`WidgetVisuals`] of its current state.
pub fn from_widget_visuals(
style: &Style,
font: impl Into<FontSelection>,
widget_visuals: &WidgetVisuals,
) -> Self {
Self::new(style, font, widget_visuals.text_color())
}
}
impl WidgetStyle for BaseStyle {}
/// Dedicated button style
#[derive(Debug, Clone)]
pub struct ButtonStyle {
/// The minimum size of the button before any per-button override.
pub min_size: Vec2,
pub frame: Frame,
pub text_style: TextVisuals,
}
@@ -77,21 +84,6 @@ pub struct CheckboxStyle {
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 {
@@ -147,3 +139,14 @@ pub struct StyleArgs<'a> {
pub style: &'a Style,
pub ctx: &'a Context,
}
impl StyleArgs<'_> {
/// Does the widget or any of its parents contain this class?
///
/// See also:
/// - [`Classes::has_class`]
/// - [`UiStack::has_class`]
pub fn has_class(&self, class: &str) -> bool {
self.classes.has_class(class) || self.stack.has_class(class)
}
}

View File

@@ -1,10 +1,9 @@
use epaint::Margin;
use crate::{
Atom, AtomExt as _, AtomKind, AtomLayout, AtomLayoutResponse, Atoms, Color32, CornerRadius,
Frame, Image, IntoAtoms, NumExt as _, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui,
Vec2, Widget, WidgetInfo, WidgetText, WidgetType,
widget_style::{ButtonStyle, Classes, HasClasses, SELECTED_CLASS, WidgetState},
Image, IntoAtoms, NumExt as _, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2,
Widget, WidgetInfo, WidgetText, WidgetType,
class::{ClassName, Classes, HasClasses},
widget_style::ButtonStyle,
};
/// Clickable button with text.
@@ -30,9 +29,6 @@ pub struct Button<'a> {
layout: AtomLayout<'a>,
fill: Option<Color32>,
stroke: Option<Stroke>,
small: bool,
frame: Option<bool>,
frame_when_inactive: bool,
min_size: Vec2,
corner_radius: Option<CornerRadius>,
selected: Option<bool>,
@@ -42,6 +38,22 @@ pub struct Button<'a> {
}
impl<'a> Button<'a> {
/// Present on a selected button.
pub const CLASS_SELECTED: ClassName = ClassName::from_static("egui::selected");
/// Present on a small button.
pub const CLASS_SMALL: ClassName = ClassName::from_static("egui::small");
/// Present on a button that should have no frame at all.
pub const CLASS_NO_FRAME: ClassName = ClassName::from_static("egui::no_frame");
/// Present on a button that should have a frame, even when the global default is frameless.
pub const CLASS_FRAME: ClassName = ClassName::from_static("egui::frame");
/// Present on a button that should have no frame while it is inactive.
pub const CLASS_HIDE_FRAME_WHEN_INACTIVE: ClassName =
ClassName::from_static("egui::button::hide_frame_when_inactive");
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
Self {
layout: AtomLayout::new(atoms.into_atoms())
@@ -49,9 +61,6 @@ impl<'a> Button<'a> {
.fallback_font(TextStyle::Button),
fill: None,
stroke: None,
small: false,
frame: None,
frame_when_inactive: true,
min_size: Vec2::ZERO,
corner_radius: None,
selected: None,
@@ -72,6 +81,8 @@ impl<'a> Button<'a> {
/// # });
/// ```
///
/// When selected, [`Self::CLASS_SELECTED`] is added.
///
/// See also:
/// - [`Ui::selectable_value`]
/// - [`Ui::selectable_label`]
@@ -142,7 +153,7 @@ impl<'a> Button<'a> {
#[inline]
pub fn fill(mut self, fill: impl Into<Color32>) -> Self {
self.fill = Some(fill.into());
self
self.frame(true)
}
/// Override button stroke. Note that this will override any on-hover effects.
@@ -150,33 +161,44 @@ impl<'a> Button<'a> {
#[inline]
pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
self.stroke = Some(stroke.into());
self.frame = Some(true);
self
self.frame(true)
}
/// Make this a small button, suitable for embedding into text.
///
/// This adds the built-in [`Self::CLASS_SMALL`], which with the default style removes the top and
/// bottom margin.
#[inline]
pub fn small(mut self) -> Self {
self.small = true;
self
pub fn small(self) -> Self {
self.with_class(Self::CLASS_SMALL)
}
/// Turn off the frame
///
/// This adds either the built-in [`Self::CLASS_FRAME`] or [`Self::CLASS_NO_FRAME`] class.
/// With the default style, the latter removes the fill, the stroke and the margin.
///
/// Default: `ui.visuals().button_frame`.
#[inline]
pub fn frame(mut self, frame: bool) -> Self {
self.frame = Some(frame);
self.set_class(Self::CLASS_FRAME, frame);
self.set_class(Self::CLASS_NO_FRAME, !frame);
self
}
/// If `false`, the button will not have a frame when inactive.
///
/// This adds the built-in [`Self::CLASS_HIDE_FRAME_WHEN_INACTIVE`], which with the
/// default style removes the fill and the stroke, but keeps the margin, so the button does
/// not change size once the user interacts with it.
///
/// Default: `true`.
///
/// Note: When [`Self::frame`] (or `ui.visuals().button_frame`) is `false`, this setting
/// has no effect.
#[inline]
pub fn frame_when_inactive(mut self, frame_when_inactive: bool) -> Self {
self.frame_when_inactive = frame_when_inactive;
self.set_class(Self::CLASS_HIDE_FRAME_WHEN_INACTIVE, !frame_when_inactive);
self
}
@@ -266,9 +288,13 @@ impl<'a> Button<'a> {
/// current pressed/not-pressed state will be reported to assistive
/// technologies (e.g. screen readers). Plain buttons that never call
/// `selected` are not announced as toggles.
///
/// When selected, [`Self::CLASS_SELECTED`] is added. You should prefer calling this though over
/// just adding [`Self::CLASS_SELECTED`] manually, since this also exposes accessibility information.
#[inline]
pub fn selected(mut self, selected: bool) -> Self {
self.selected = Some(selected);
self.set_class(Self::CLASS_SELECTED, selected);
self
}
@@ -292,22 +318,14 @@ impl<'a> Button<'a> {
mut layout,
fill,
stroke,
small,
frame,
frame_when_inactive,
mut min_size,
corner_radius,
selected,
image_tint_follows_text_color,
limit_image_size,
mut classes,
classes,
} = self;
// Min size height always equal or greater than interact size if not small
if !small {
min_size.y = min_size.y.at_least(ui.spacing().interact_size.y);
}
if limit_image_size {
layout.map_atoms(|atom| {
if matches!(&atom.kind, AtomKind::Image(_)) {
@@ -320,29 +338,16 @@ impl<'a> Button<'a> {
let text = layout.text().map(String::from);
let has_frame_margin = frame.unwrap_or_else(|| ui.visuals().button_frame);
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 ButtonStyle {
mut frame,
text_style,
min_size: style_min_size,
} = ui.widget_style(id, &classes);
classes.add_class_if(SELECTED_CLASS, selected.unwrap_or(false));
let ButtonStyle { frame, text_style } = ui.widget_style(id, &classes);
let mut button_padding = if has_frame_margin {
frame.inner_margin
} else {
Margin::ZERO
};
if small {
button_padding.bottom = 0;
button_padding.top = 0;
}
min_size = min_size.at_least(style_min_size);
// Override global style by local style
let mut frame = frame;
if let Some(fill) = fill {
frame = frame.fill(fill);
}
@@ -353,21 +358,12 @@ impl<'a> Button<'a> {
frame = frame.stroke(stroke);
}
frame = frame.inner_margin(button_padding);
// Apply the style font and color as fallback
layout = layout
.fallback_font(text_style.font_id.clone())
.fallback_text_color(text_style.color);
// Retrocompatibility with button settings
layout = if has_frame_margin && (state != WidgetState::Inactive || frame_when_inactive) {
layout.frame(frame)
} else {
layout.frame(Frame::new().inner_margin(frame.inner_margin))
};
let mut prepared = layout.min_size(min_size).allocate(ui);
let mut prepared = layout.frame(frame).min_size(min_size).allocate(ui);
// Get AtomLayoutResponse, empty if not visible
let response = if ui.is_rect_visible(prepared.response.rect) {

View File

@@ -2,8 +2,10 @@ use emath::Rect;
use crate::{
Atom, AtomLayout, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Shape, Ui, Vec2, Widget,
WidgetInfo, WidgetType, epaint, pos2,
widget_style::{CheckboxStyle, Classes, HasClasses},
WidgetInfo, WidgetType,
class::{Classes, HasClasses},
epaint, pos2,
widget_style::CheckboxStyle,
};
// TODO(emilk): allow checkbox without a text label

View File

@@ -1,6 +1,8 @@
use crate::{
Response, Sense, Ui, Vec2, Widget, vec2,
widget_style::{Classes, HasClasses, SeparatorStyle},
Response, Sense, Ui, Vec2, Widget,
class::{Classes, HasClasses},
vec2,
widget_style::SeparatorStyle,
};
/// A visual separator. A horizontal or vertical line (depending on [`crate::Layout`]).

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:24fc466f6470761a5064a590929fa3a0d95741ed691fc257415eee349bc528e9
size 101394
oid sha256:6264f32d3323d0d499ba0da9d379e562c06a7f50930dbec424039549ce7f50f9
size 101414