From 64470b2d40023e1ef0fa906cb17d6528a17f659c Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Mon, 31 Aug 2026 13:49:10 +0200 Subject: [PATCH] Add `Frame::expand_in_place`, `Frame::invisible` and `TextVisuals` constructors The expansion/stroke correction on a widget's margins was written out by hand. Move it into `Frame::expand_in_place`, which knows the frame's own stroke width, so a caller states the intent instead of the arithmetic. `Frame::invisible` goes with it: a frame that keeps its layout but drops its paint. Two fixes fall out of it in the button style: * A small button zeroed its vertical padding after the correction, which left the frame a `stroke.width - expansion` tall. Zeroing before the correction makes it exactly zero in every state, which is what "must not add any height" means. * The invisible frame of a button that hides its frame when inactive dropped the outer margin and the stroke width, so it was not, in fact, as big as the painted one. It now keeps the whole frame and only drops the paint. Also give `TextVisuals` a `new` and a `from_widget_visuals`, replacing the local `text_visuals` helper, and add `HasClasses::add_classes` and `HasClasses::with_classes`. Co-Authored-By: Claude Opus 5 (1M context) --- crates/egui/src/containers/frame.rs | 22 ++++++++++++ crates/egui/src/theme/default_style.rs | 48 ++++++++++--------------- crates/egui/src/widget_style/classes.rs | 18 ++++++++++ crates/egui/src/widget_style/mod.rs | 26 +++++++++++++- examples/styling_engine/src/main.rs | 7 ++-- 5 files changed, 85 insertions(+), 36 deletions(-) diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index ebef299df..731c48f23 100644 --- a/crates/egui/src/containers/frame.rs +++ b/crates/egui/src/containers/frame.rs @@ -298,6 +298,18 @@ impl Frame { self } + /// Expand the frame without affecting layout. + /// + /// This handles `expansion` by subtracting it from the outer margin and adding it to the + /// inner margin. It also corrects for a stroke changing on hover, by subtracting the stroke + /// width from `inner_margin`. + #[inline] + pub fn expand_in_place(mut self, expansion: f32) -> Self { + self.outer_margin = self.outer_margin - Margin::from(expansion); + self.inner_margin = self.inner_margin + Margin::from(expansion - self.stroke.width); + self + } + /// Optional drop-shadow behind the frame. #[inline] pub fn shadow(mut self, shadow: Shadow) -> Self { @@ -316,6 +328,16 @@ 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. + pub fn invisible(mut self) -> Self { + self.fill = Color32::TRANSPARENT; + self.stroke.color = Color32::TRANSPARENT; + self.shadow = Shadow::NONE; + self + } } /// ## Inspectors diff --git a/crates/egui/src/theme/default_style.rs b/crates/egui/src/theme/default_style.rs index 9c95d981a..571945793 100644 --- a/crates/egui/src/theme/default_style.rs +++ b/crates/egui/src/theme/default_style.rs @@ -2,8 +2,7 @@ use emath::Vec2; use epaint::Margin; use crate::{ - Button, Context, Frame, Style, TextStyle, - style::WidgetVisuals, + Button, Context, Frame, TextStyle, theme::StyleProvider, widget_style::{ ButtonStyle, CheckboxStyle, HasClasses as _, SeparatorStyle, StyleArgs, TextVisuals, @@ -30,17 +29,6 @@ impl DefaultStyle { } } -/// The text of a widget, based on the [`WidgetVisuals`] of its current state. -fn text_visuals(style: &Style, widget_visuals: &WidgetVisuals) -> TextVisuals { - TextVisuals { - color: widget_visuals.text_color(), - font_id: style - .override_font_id - .clone() - .unwrap_or_else(|| TextStyle::Body.resolve(style)), - } -} - impl StyleProvider for DefaultStyle { fn style(&mut self, modifiers: &StyleArgs<'_>) -> ButtonStyle { let StyleArgs { @@ -59,17 +47,24 @@ impl StyleProvider for DefaultStyle { widget_visuals.fg_stroke = visuals.selection.stroke; } - let mut inner_margin: Margin = (spacing.button_padding - + Vec2::splat(widget_visuals.expansion) - - Vec2::splat(widget_visuals.bg_stroke.width)) - .into(); + let mut inner_margin: Margin = spacing.button_padding.into(); - // A small button is meant to be embedded into text, so it must not add any height. + // 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, + stroke: widget_visuals.bg_stroke, + corner_radius: widget_visuals.corner_radius, + inner_margin, + ..Default::default() + } + // Ensure changing expansion and stroke don't affect layout: + .expand_in_place(widget_visuals.expansion); + let has_frame = classes.has_class(Button::CLASS_FRAME) || (!classes.has_class(Button::CLASS_NO_FRAME) && style.visuals.button_frame); @@ -79,17 +74,10 @@ impl StyleProvider for DefaultStyle { } else if classes.has_class(Button::CLASS_HIDE_FRAME_WHEN_INACTIVE) && *state == WidgetState::Inactive { - // Invisible, but as big as it will be once the user interacts with it. - Frame::new().inner_margin(inner_margin) + // Hide the frame, but keep its spacing + painted_frame.invisible() } else { - 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, - ..Default::default() - } + painted_frame }; ButtonStyle { @@ -99,7 +87,7 @@ impl StyleProvider for DefaultStyle { Vec2::new(0.0, spacing.interact_size.y) }, frame, - text_style: text_visuals(style, &widget_visuals), + text_style: TextVisuals::from_widget_visuals(style, TextStyle::Body, &widget_visuals), } } } @@ -120,7 +108,7 @@ impl StyleProvider for DefaultStyle { stroke: widget_visuals.bg_stroke, ..Default::default() }, - text_style: text_visuals(style, &widget_visuals), + 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/classes.rs b/crates/egui/src/widget_style/classes.rs index 4d76fee25..41aabf3f4 100644 --- a/crates/egui/src/widget_style/classes.rs +++ b/crates/egui/src/widget_style/classes.rs @@ -78,6 +78,18 @@ pub trait HasClasses { self } + /// Add all the given classes by consuming `self` + /// + /// Useful to forward the classes of a composite widget to the widgets it is built from. + #[inline] + fn with_classes(mut self, classes: Classes) -> Self + where + Self: Sized, + { + self.classes_mut().classes.extend(classes.classes); + self + } + /// Add the given class by consuming `self` if the condition is true #[inline] fn with_class_if(mut self, class: impl Into, condition: bool) -> Self @@ -98,6 +110,12 @@ pub trait HasClasses { self } + #[inline] + fn add_classes(&mut self, classes: Classes) -> &mut Self { + self.classes_mut().classes.extend(classes.classes); + self + } + /// Add the given class in-place if the condition is true #[inline] fn add_class_if(&mut self, class: impl Into, condition: bool) -> &mut Self diff --git a/crates/egui/src/widget_style/mod.rs b/crates/egui/src/widget_style/mod.rs index f945116b9..1a28327d9 100644 --- a/crates/egui/src/widget_style/mod.rs +++ b/crates/egui/src/widget_style/mod.rs @@ -17,7 +17,7 @@ use core::fmt::Debug; use epaint::{Color32, FontId, Stroke, Vec2}; use crate::{ - Context, Frame, Response, Style, UiStack, + Context, Frame, Response, Style, TextStyle, UiStack, style::{WidgetVisuals, Widgets}, }; @@ -34,6 +34,30 @@ pub struct TextVisuals { pub color: Color32, } +impl TextVisuals { + /// Text in `color`, using the font of the given [`TextStyle`]. + /// + /// `style.override_font_id` wins over `text_style`, if it is set. + pub fn new(style: &Style, text_style: TextStyle, color: Color32) -> Self { + Self { + color, + font_id: style + .override_font_id + .clone() + .unwrap_or_else(|| text_style.resolve(style)), + } + } + + /// The text of a widget, colored by the [`WidgetVisuals`] of its current state. + pub fn from_widget_visuals( + style: &Style, + text_style: TextStyle, + widget_visuals: &WidgetVisuals, + ) -> Self { + Self::new(style, text_style, widget_visuals.text_color()) + } +} + /// Dedicated button style #[derive(Debug, Clone)] pub struct ButtonStyle { diff --git a/examples/styling_engine/src/main.rs b/examples/styling_engine/src/main.rs index 43a87710a..6ba03a9bc 100644 --- a/examples/styling_engine/src/main.rs +++ b/examples/styling_engine/src/main.rs @@ -65,11 +65,8 @@ impl StyleProvider for MyTheme { .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), - }, + // Resolve the font from the style, so we follow the user's font sizes: + text_style: TextVisuals::new(style, TextStyle::Button, Color32::WHITE), } } }