diff --git a/crates/egui/src/theme/default_style.rs b/crates/egui/src/theme/default_style.rs index a6a89a230..877132e78 100644 --- a/crates/egui/src/theme/default_style.rs +++ b/crates/egui/src/theme/default_style.rs @@ -6,7 +6,8 @@ use crate::{ class::HasClasses as _, theme::StyleProvider, widget_style::{ - ButtonStyle, CheckboxStyle, SeparatorStyle, StyleArgs, TextVisuals, WidgetState, + AtomLayoutStyle, ButtonStyle, CheckboxStyle, SeparatorStyle, StyleArgs, TextVisuals, + WidgetState, }, }; @@ -80,14 +81,26 @@ impl StyleProvider for DefaultStyle { painted_frame }; + let text_style = TextVisuals::from_widget_visuals(style, TextStyle::Body, &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: TextVisuals::from_widget_visuals(style, TextStyle::Body, &widget_visuals), } } } @@ -99,7 +112,17 @@ 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: TextVisuals::from_widget_visuals( + style, + TextStyle::Body, + &widget_visuals, + ), + ..Default::default() + }, checkbox_size: spacing.icon_width, check_size: spacing.icon_width_inner, checkbox_frame: Frame { @@ -108,7 +131,6 @@ impl StyleProvider for DefaultStyle { stroke: widget_visuals.bg_stroke, ..Default::default() }, - text_style: TextVisuals::from_widget_visuals(style, TextStyle::Body, &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 46ee4e132..f6061529b 100644 --- a/crates/egui/src/widget_style/mod.rs +++ b/crates/egui/src/widget_style/mod.rs @@ -3,10 +3,12 @@ #![cfg_attr(not(feature = "experimental"), allow(dead_code, unused_imports))] use core::fmt::Debug; -use epaint::{Color32, FontId, Stroke, Vec2}; + +use emath::{Align2, Vec2}; +use epaint::{Color32, FontId, Stroke}; use crate::{ - Context, FontSelection, Frame, Response, Style, UiStack, + AtomLayout, Context, FontSelection, Frame, Response, Style, UiStack, class::{Classes, HasClasses as _}, style::{WidgetVisuals, Widgets}, }; @@ -48,14 +50,76 @@ impl TextVisuals { } } +/// 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 {} @@ -63,11 +127,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 a385e5b18..8bbee9d79 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -32,7 +32,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, } @@ -54,6 +53,10 @@ impl<'a> Button<'a> { pub const CLASS_HIDE_FRAME_WHEN_INACTIVE: ClassName = ClassName::from_static("egui::button::hide_frame_when_inactive"); + /// Present when untinted images should follow the button text color. + pub const CLASS_IMAGE_TINT_FOLLOWS_TEXT_COLOR: ClassName = + ClassName::from_static("egui::button::image_tint_follows_text_color"); + pub fn new(atoms: impl IntoAtoms<'a>) -> Self { Self { layout: AtomLayout::new(atoms.into_atoms()) @@ -64,7 +67,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 f1c90fa4c..a5c5185ab 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -73,16 +73,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); @@ -92,11 +90,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; @@ -120,7 +115,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 6442b50fb..4405092a8 100644 --- a/examples/styling_engine/src/main.rs +++ b/examples/styling_engine/src/main.rs @@ -7,7 +7,7 @@ use eframe::egui::{ self, CentralPanel, Color32, Frame, Panel, TextStyle, class::HasClasses as _, theme::StyleProvider, - widget_style::{ButtonStyle, StyleArgs, TextVisuals, WidgetState}, + widget_style::{AtomLayoutStyle, ButtonStyle, StyleArgs, TextVisuals, WidgetState}, }; /// Buttons with this class are styled as a destructive action. @@ -61,13 +61,17 @@ 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), - // Resolve the font from the style, so we follow the user's font sizes: - text_style: TextVisuals::new(style, TextStyle::Button, Color32::WHITE), + 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), + // Resolve the font from the style, so we follow the user's font sizes: + text_style: TextVisuals::new(style, TextStyle::Button, Color32::WHITE), + ..Default::default() + }, } } }