diff --git a/crates/egui/src/theme/default_style.rs b/crates/egui/src/theme/default_style.rs index 196bf2280..c4560db7b 100644 --- a/crates/egui/src/theme/default_style.rs +++ b/crates/egui/src/theme/default_style.rs @@ -6,8 +6,8 @@ use crate::{ style::WidgetVisuals, theme::StyleProvider, widget_style::{ - ButtonStyle, CheckboxStyle, HasClasses as _, SeparatorStyle, StyleArgs, TextVisuals, - WidgetState, + AtomLayoutStyle, ButtonStyle, CheckboxStyle, HasClasses as _, SeparatorStyle, StyleArgs, + TextVisuals, WidgetState, }, }; @@ -78,14 +78,26 @@ impl StyleProvider for DefaultStyle { } }; + let text_style = text_visuals(style, &widget_visuals); + let image_tint = if classes.has_class(Button::CLASS_IMAGE_TINT_FOLLOWS_TEXT_COLOR) { + text_style.color + } else { + crate::Color32::WHITE + }; + ButtonStyle { - min_size: if classes.has_class(Button::CLASS_SMALL) { - Vec2::ZERO - } else { - Vec2::new(0.0, spacing.interact_size.y) + atom_layout: AtomLayoutStyle { + min_size: if classes.has_class(Button::CLASS_SMALL) { + Vec2::ZERO + } else { + Vec2::new(0.0, spacing.interact_size.y) + }, + gap: spacing.icon_spacing, + frame, + text_style, + image_tint, + ..Default::default() }, - frame, - text_style: text_visuals(style, &widget_visuals), } } } @@ -97,7 +109,13 @@ impl StyleProvider for DefaultStyle { let widget_visuals = *style.visuals.widgets.state(*state); CheckboxStyle { - frame: Frame::new(), + atom_layout: AtomLayoutStyle { + min_size: Vec2::splat(spacing.interact_size.y), + gap: spacing.icon_spacing, + frame: Frame::new(), + text_style: text_visuals(style, &widget_visuals), + ..Default::default() + }, checkbox_size: spacing.icon_width, check_size: spacing.icon_width_inner, checkbox_frame: Frame { @@ -106,7 +124,6 @@ impl StyleProvider for DefaultStyle { stroke: widget_visuals.bg_stroke, ..Default::default() }, - text_style: text_visuals(style, &widget_visuals), check_stroke: widget_visuals.fg_stroke, } } diff --git a/crates/egui/src/widget_style/mod.rs b/crates/egui/src/widget_style/mod.rs index f945116b9..3ae153a3b 100644 --- a/crates/egui/src/widget_style/mod.rs +++ b/crates/egui/src/widget_style/mod.rs @@ -14,10 +14,11 @@ pub mod class { use core::fmt::Debug; -use epaint::{Color32, FontId, Stroke, Vec2}; +use emath::{Align2, Vec2}; +use epaint::{Color32, FontId, Stroke}; use crate::{ - Context, Frame, Response, Style, UiStack, + AtomLayout, Context, Frame, Response, Style, UiStack, style::{WidgetVisuals, Widgets}, }; @@ -34,14 +35,76 @@ pub struct TextVisuals { pub color: Color32, } +/// Visual and layout style shared by widgets built from an [`AtomLayout`]. +#[derive(Debug, Clone)] +pub struct AtomLayoutStyle { + /// Alignment of the atoms within the allocated rectangle. + /// + /// `None` uses the alignment of the surrounding [`crate::Ui`]. + pub align2: Option, + + /// Minimum size of the atom layout. + pub min_size: Vec2, + + /// Space between adjacent atoms. + pub gap: f32, + + /// Frame around the atoms. + pub frame: Frame, + + /// Fallback visuals for text atoms. + pub text_style: TextVisuals, + + /// Fallback tint for images whose tint is [`Color32::WHITE`] (untinted). + pub image_tint: Color32, +} + +impl Default for AtomLayoutStyle { + fn default() -> Self { + Self { + align2: None, + min_size: Vec2::ZERO, + gap: 0.0, + frame: Frame::default(), + text_style: TextVisuals { + font_id: FontId::default(), + color: Color32::WHITE, + }, + image_tint: Color32::WHITE, + } + } +} + +impl AtomLayoutStyle { + /// Apply this style to an [`AtomLayout`]. + pub fn apply<'a>(&self, mut layout: AtomLayout<'a>) -> AtomLayout<'a> { + layout.map_images(|image| { + if image.image_options().tint == Color32::WHITE { + image.tint(self.image_tint) + } else { + image + } + }); + + let layout = layout + .min_size(self.min_size) + .gap(self.gap) + .frame(self.frame) + .fallback_font(self.text_style.font_id.clone()) + .fallback_text_color(self.text_style.color); + + if let Some(align2) = self.align2 { + layout.align2(align2) + } else { + layout + } + } +} + /// 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, + pub atom_layout: AtomLayoutStyle, } impl WidgetStyle for ButtonStyle {} @@ -49,11 +112,8 @@ 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, + /// Style of the checkbox's atom layout. + pub atom_layout: AtomLayoutStyle, /// Checkbox size pub checkbox_size: f32, diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 771f34d65..3b0fa324d 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -31,7 +31,6 @@ pub struct Button<'a> { min_size: Vec2, corner_radius: Option, selected: Option, - image_tint_follows_text_color: bool, limit_image_size: bool, classes: Classes, } @@ -53,6 +52,10 @@ impl<'a> Button<'a> { pub const CLASS_HIDE_FRAME_WHEN_INACTIVE: &'static str = "egui::button::hide_frame_when_inactive"; + /// Present when untinted images should follow the button text color. + pub const CLASS_IMAGE_TINT_FOLLOWS_TEXT_COLOR: &'static str = + "egui::button::image_tint_follows_text_color"; + pub fn new(atoms: impl IntoAtoms<'a>) -> Self { Self { layout: AtomLayout::new(atoms.into_atoms()) @@ -63,7 +66,6 @@ impl<'a> Button<'a> { min_size: Vec2::ZERO, corner_radius: None, selected: None, - image_tint_follows_text_color: false, limit_image_size: false, classes: Classes::default(), } @@ -224,15 +226,19 @@ impl<'a> Button<'a> { self } - /// If true, the tint of the image is multiplied by the widget text color. + /// If true, use the widget text color as the fallback tint for images. /// - /// This makes sense for images that are white, that should have the same color as the text color. - /// This will also make the icon color depend on hover state. + /// This makes sense for monochrome images that should have the same color as the text. It also + /// makes the image color depend on hover state. A non-white tint set on an image takes + /// precedence over this fallback; [`Color32::WHITE`] means untinted. /// /// Default: `false`. #[inline] pub fn image_tint_follows_text_color(mut self, image_tint_follows_text_color: bool) -> Self { - self.image_tint_follows_text_color = image_tint_follows_text_color; + self.set_class( + Self::CLASS_IMAGE_TINT_FOLLOWS_TEXT_COLOR, + image_tint_follows_text_color, + ); self } @@ -318,10 +324,9 @@ impl<'a> Button<'a> { mut layout, fill, stroke, - mut min_size, + min_size, corner_radius, selected, - image_tint_follows_text_color, limit_image_size, classes, } = self; @@ -340,39 +345,29 @@ impl<'a> Button<'a> { let id = ui.next_auto_id(); let ButtonStyle { - mut frame, - text_style, - min_size: style_min_size, + atom_layout: mut atom_layout_style, } = ui.widget_style(id, &classes); - min_size = min_size.at_least(style_min_size); + let min_size = min_size.at_least(atom_layout_style.min_size); // Override global style by local style + if let Some(stroke) = stroke { + atom_layout_style.frame = atom_layout_style.frame.stroke(stroke); + } if let Some(fill) = fill { - frame = frame.fill(fill); + atom_layout_style.frame = atom_layout_style.frame.fill(fill); } if let Some(corner_radius) = corner_radius { - frame = frame.corner_radius(corner_radius); - } - if let Some(stroke) = stroke { - frame = frame.stroke(stroke); + atom_layout_style.frame = atom_layout_style.frame.corner_radius(corner_radius); } - // Apply the style font and color as fallback - layout = layout - .fallback_font(text_style.font_id.clone()) - .fallback_text_color(text_style.color); - - let mut prepared = layout.frame(frame).min_size(min_size).allocate(ui); + let prepared = atom_layout_style + .apply(layout) + .min_size(min_size) + .allocate(ui); // Get AtomLayoutResponse, empty if not visible let response = if ui.is_rect_visible(prepared.response.rect) { - if image_tint_follows_text_color { - prepared.map_images(|image| image.tint(text_style.color)); - } - - prepared.fallback_text_color = text_style.color; - prepared.paint(ui) } else { AtomLayoutResponse::empty(prepared.response) diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index 9310c64d5..7cc344065 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -71,16 +71,14 @@ impl Widget for Checkbox<'_> { // Get the widget style by reading the response from the previous pass let id = ui.next_auto_id(); let CheckboxStyle { + atom_layout, check_size, checkbox_frame, checkbox_size, - frame, check_stroke, - text_style, } = ui.widget_style(id, &classes); - let mut min_size = Vec2::splat(ui.spacing().interact_size.y); - min_size.y = min_size.y.at_least(checkbox_size); + let min_size = atom_layout.min_size.at_least(Vec2::new(0.0, checkbox_size)); // In order to center the checkbox based on min_size we set the icon height to at least min_size.y let mut icon_size = Vec2::splat(checkbox_size); @@ -90,11 +88,8 @@ impl Widget for Checkbox<'_> { let text = atoms.text().map(String::from); - let mut prepared = AtomLayout::new(atoms) - .sense(Sense::click()) - .min_size(min_size) - .frame(frame) - .allocate(ui); + let layout = AtomLayout::new(atoms).sense(Sense::click()); + let mut prepared = atom_layout.apply(layout).min_size(min_size).allocate(ui); if prepared.response.clicked() { *checked = !*checked; @@ -118,7 +113,6 @@ impl Widget for Checkbox<'_> { }); if ui.is_rect_visible(prepared.response.rect) { - prepared.fallback_text_color = text_style.color; let response = prepared.paint(ui); if let Some(rect) = response.rect(rect_id) { diff --git a/examples/styling_engine/src/main.rs b/examples/styling_engine/src/main.rs index 43a87710a..cb8f4381a 100644 --- a/examples/styling_engine/src/main.rs +++ b/examples/styling_engine/src/main.rs @@ -6,7 +6,9 @@ use eframe::egui::{ self, CentralPanel, Color32, Frame, Panel, TextStyle, theme::StyleProvider, - widget_style::{ButtonStyle, HasClasses as _, StyleArgs, TextVisuals, WidgetState}, + widget_style::{ + AtomLayoutStyle, ButtonStyle, HasClasses as _, StyleArgs, TextVisuals, WidgetState, + }, }; /// Buttons with this class are styled as a destructive action. @@ -60,15 +62,19 @@ impl StyleProvider 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: TextVisuals { - color: Color32::WHITE, - // Resolve the font from the style, so we follow the user's font sizes: - font_id: TextStyle::Button.resolve(style), + atom_layout: AtomLayoutStyle { + min_size: egui::vec2(0.0, style.spacing.interact_size.y), + gap: style.spacing.icon_spacing, + frame: Frame::new() + .fill(fill) + .corner_radius(self.corner_radius) + .inner_margin(8), + text_style: TextVisuals { + color: Color32::WHITE, + // Resolve the font from the style, so we follow the user's font sizes: + font_id: TextStyle::Button.resolve(style), + }, + ..Default::default() }, } }