mirror of
https://github.com/emilk/egui.git
synced 2026-09-01 06:10:06 -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:
@@ -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`
|
||||
|
||||
147
crates/egui/src/class/class_name.rs
Normal file
147
crates/egui/src/class/class_name.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
104
crates/egui/src/class/classes.rs
Normal file
104
crates/egui/src/class/classes.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
140
crates/egui/src/class/has_classes.rs
Normal file
140
crates/egui/src/class/has_classes.rs
Normal 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"]);
|
||||
}
|
||||
}
|
||||
10
crates/egui/src/class/mod.rs
Normal file
10
crates/egui/src/class/mod.rs
Normal 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");
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -437,6 +437,7 @@ pub mod widgets;
|
||||
#[cfg(feature = "callstack")]
|
||||
#[cfg(debug_assertions)]
|
||||
mod callstack;
|
||||
pub mod class;
|
||||
|
||||
pub use accesskit;
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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`
|
||||
///
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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`].
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`]).
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:24fc466f6470761a5064a590929fa3a0d95741ed691fc257415eee349bc528e9
|
||||
size 101394
|
||||
oid sha256:6264f32d3323d0d499ba0da9d379e562c06a7f50930dbec424039549ce7f50f9
|
||||
size 101414
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
//! based on the _classes_ set on it, and that can be edited live.
|
||||
|
||||
use eframe::egui::{
|
||||
self, CentralPanel, Color32, Frame, Panel,
|
||||
self, CentralPanel, Color32, Frame, Panel, TextStyle,
|
||||
class::HasClasses as _,
|
||||
theme::StyleProvider,
|
||||
widget_style::{BaseStyle, ButtonStyle, HasClasses as _, StyleArgs, WidgetState},
|
||||
widget_style::{ButtonStyle, StyleArgs, TextVisuals, WidgetState},
|
||||
};
|
||||
|
||||
/// Buttons with this class are styled as a destructive action.
|
||||
@@ -42,14 +43,11 @@ impl StyleProvider<ButtonStyle> for MyTheme {
|
||||
let StyleArgs {
|
||||
classes,
|
||||
state,
|
||||
ctx,
|
||||
style,
|
||||
..
|
||||
} = 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) {
|
||||
let fill = if classes.has_class(DANGER) {
|
||||
self.danger
|
||||
} else {
|
||||
self.normal
|
||||
@@ -63,11 +61,13 @@ impl StyleProvider<ButtonStyle> for MyTheme {
|
||||
};
|
||||
|
||||
ButtonStyle {
|
||||
min_size: egui::vec2(0.0, style.spacing.interact_size.y),
|
||||
frame: Frame::new()
|
||||
.fill(fill)
|
||||
.corner_radius(self.corner_radius)
|
||||
.inner_margin(8),
|
||||
text_style: base.text,
|
||||
// Resolve the font from the style, so we follow the user's font sizes:
|
||||
text_style: TextVisuals::new(style, TextStyle::Button, Color32::WHITE),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ fn test_atom_layout_nesting_and_direction() {
|
||||
|
||||
let button_frame = ui
|
||||
.get_widget_style::<ButtonStyle>(&StyleArgs {
|
||||
classes: &egui::widget_style::Classes::default(),
|
||||
classes: &egui::class::Classes::default(),
|
||||
state: egui::widget_style::WidgetState::Inactive,
|
||||
ctx: ui,
|
||||
stack: ui.stack(),
|
||||
|
||||
Reference in New Issue
Block a user