From 887d9b041562eb20fc32542734e0bdd251822e32 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 25 Aug 2026 10:40:35 +0200 Subject: [PATCH] Give TextEdit a min_size and a gap, via a shared LayoutStyle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ButtonStyle` carried a `min_size` and a gap; `TextEditStyle` carried neither, so a themed text edit could set its padding but not its height or the gap to its prefix — the height had to travel separately on the widget, and the gap fell back to the ambient `spacing.icon_spacing`. A theme decides the two together, so they move into a `LayoutStyle` that both styles embed. `TextEdit` now honors both, with defaults that keep today's rendering: no floor of egui's own, and the gap `AtomLayout` was already falling back to. Co-Authored-By: Claude Opus 5 (1M context) --- crates/egui/src/theme/default_style.rs | 21 +++++++--- crates/egui/src/widget_style/mod.rs | 42 +++++++++++++++----- crates/egui/src/widgets/button.rs | 9 +++-- crates/egui/src/widgets/text_edit/builder.rs | 12 +++++- examples/styling_engine/src/main.rs | 8 ++-- 5 files changed, 68 insertions(+), 24 deletions(-) diff --git a/crates/egui/src/theme/default_style.rs b/crates/egui/src/theme/default_style.rs index f68ae63eb..0cb665f3f 100644 --- a/crates/egui/src/theme/default_style.rs +++ b/crates/egui/src/theme/default_style.rs @@ -5,8 +5,9 @@ use crate::{ Frame, TextStyle, theme::StyleProvider, widget_style::{ - BaseStyle, ButtonStyle, CheckboxStyle, HasClasses as _, LabelStyle, READ_ONLY_CLASS, - SELECTED_CLASS, SeparatorStyle, StyleArgs, TextEditStyle, TextVisuals, WidgetState, + BaseStyle, ButtonStyle, CheckboxStyle, HasClasses as _, LabelStyle, LayoutStyle, + READ_ONLY_CLASS, SELECTED_CLASS, SeparatorStyle, StyleArgs, TextEditStyle, TextVisuals, + WidgetState, }, }; @@ -87,11 +88,13 @@ impl StyleProvider for DefaultStyle { .into(), ..Default::default() }, + layout: LayoutStyle { + // Historically only the height was floored, so that a button is at least as tall + // as any other interactive widget on the same row. + min_size: Vec2::new(0.0, spacing.interact_size.y), + gap: spacing.icon_spacing, + }, text_style: ws.text, - // Historically only the height was floored, so that a button is at least as tall as - // any other interactive widget on the same row. - min_size: Vec2::new(0.0, spacing.interact_size.y), - gap: spacing.icon_spacing, } } } @@ -145,6 +148,12 @@ impl StyleProvider for DefaultStyle { outer_margin: Margin::same(-(widget_visuals.expansion as i8)), ..Default::default() }, + layout: LayoutStyle { + // A text edit sizes itself from the rows it holds; egui's own theme adds no floor + // of its own. + min_size: Vec2::ZERO, + gap: style.spacing.icon_spacing, + }, text: ws.text, hint_text_color: style.visuals.weak_text_color(), } diff --git a/crates/egui/src/widget_style/mod.rs b/crates/egui/src/widget_style/mod.rs index 8201735a5..17dcfbe79 100644 --- a/crates/egui/src/widget_style/mod.rs +++ b/crates/egui/src/widget_style/mod.rs @@ -65,22 +65,35 @@ pub struct BaseStyle { impl WidgetStyle for BaseStyle {} +/// How a widget's contents are laid out +/// +/// A theme that gives a widget a size decides both together — the height and the gap of a small +/// button are one look, not two — so they travel as one struct, shared by every widget style that +/// lays its contents out with an [`crate::AtomLayout`]. +#[derive(Debug, Clone)] +pub struct LayoutStyle { + /// How small the widget may get, before its contents are taken into account. + /// + /// A floor, not a size: a widget is never smaller than what it holds. + pub min_size: Vec2, + + /// The gap between the widget's atoms, e.g. between an icon and the text beside it. + pub gap: f32, +} + /// Dedicated button style #[derive(Debug, Clone)] pub struct ButtonStyle { pub frame: Frame, + + /// How the button's contents are laid out. + /// + /// [`LayoutStyle::min_size`] is ignored by a [`crate::Button::small`] button, which sizes + /// itself purely from its contents and its own [`crate::Button::min_size`]; + /// [`LayoutStyle::gap`] is overridden by [`crate::Button::gap`]. + pub layout: LayoutStyle, + pub text_style: TextVisuals, - - /// How small the button may get, before its contents are taken into account. - /// - /// Ignored by a [`crate::Button::small`] button, which sizes itself purely from its contents - /// and its own [`crate::Button::min_size`]. - pub min_size: Vec2, - - /// The gap between the button's atoms, e.g. between its icon and its text. - /// - /// Overridden by [`crate::Button::gap`]. - pub gap: f32, } impl WidgetStyle for ButtonStyle {} @@ -91,6 +104,13 @@ pub struct TextEditStyle { /// Frame around the text, including its padding. pub frame: Frame, + /// How the field's contents are laid out. + /// + /// [`LayoutStyle::min_size`] is raised by [`crate::TextEdit::min_size`] and by the rows of + /// text the field holds, so it only ever sets a floor; [`LayoutStyle::gap`] separates the + /// text from a [`crate::TextEdit::prefix`] or [`crate::TextEdit::suffix`]. + pub layout: LayoutStyle, + /// The text being edited. pub text: TextVisuals, diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 29cf68eed..c646214af 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -4,7 +4,7 @@ use crate::{ Atom, AtomExt as _, AtomKind, AtomLayout, AtomLayoutResponse, Atoms, Color32, CornerRadius, Frame, Image, IntoAtoms, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType, - widget_style::{ButtonStyle, Classes, HasClasses, SELECTED_CLASS, WidgetState}, + widget_style::{ButtonStyle, Classes, HasClasses, LayoutStyle, SELECTED_CLASS, WidgetState}, }; /// Clickable button with text. @@ -325,9 +325,12 @@ impl<'a> Button<'a> { let ButtonStyle { frame, + layout: + LayoutStyle { + min_size: style_min_size, + gap, + }, text_style, - min_size: style_min_size, - gap, } = ui.widget_style(id, &classes); layout = layout.fallback_gap(gap); diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 1af9b0fd8..c443c1a2f 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -17,7 +17,7 @@ use crate::{ self, CCursorRange, text_cursor_state::cursor_rect, visuals::paint_text_selection, }, vec2, - widget_style::{Classes, HasClasses, READ_ONLY_CLASS, TextEditStyle}, + widget_style::{Classes, HasClasses, LayoutStyle, READ_ONLY_CLASS, TextEditStyle}, }; use super::{TextEditOutput, TextEditState}; @@ -489,10 +489,19 @@ impl TextEdit<'_> { classes.add_class_if(READ_ONLY_CLASS, !text.is_mutable()); let TextEditStyle { frame: styled_frame, + layout: + LayoutStyle { + min_size: style_min_size, + gap, + }, text: text_visuals, hint_text_color, } = ui.widget_style(id, &classes); + // The theme sets a floor on the size; the builder's own `min_size` can only raise it, + // the same way it does for a button. + let min_size = min_size.max(style_min_size); + let text_color = text_color .or_else(|| ui.visuals().override_text_color) .unwrap_or(text_visuals.color); @@ -736,6 +745,7 @@ impl TextEdit<'_> { let allocated = AtomLayout::new(atoms) .id(id) + .fallback_gap(gap) .min_size(Vec2::new(allocate_width, min_height.at_least(min_size.y))) .max_width(allocate_width) .sense(sense) diff --git a/examples/styling_engine/src/main.rs b/examples/styling_engine/src/main.rs index 275cc59fc..425578f38 100644 --- a/examples/styling_engine/src/main.rs +++ b/examples/styling_engine/src/main.rs @@ -6,7 +6,7 @@ use eframe::egui::{ self, CentralPanel, Color32, Frame, Panel, theme::StyleProvider, - widget_style::{BaseStyle, ButtonStyle, HasClasses as _, StyleArgs, WidgetState}, + widget_style::{BaseStyle, ButtonStyle, HasClasses as _, LayoutStyle, StyleArgs, WidgetState}, }; /// Buttons with this class are styled as a destructive action. @@ -67,9 +67,11 @@ impl StyleProvider for MyTheme { .fill(fill) .corner_radius(self.corner_radius) .inner_margin(8), + layout: LayoutStyle { + min_size: args.style.spacing.interact_size, + gap: args.style.spacing.icon_spacing, + }, text_style: base.text, - min_size: args.style.spacing.interact_size, - gap: args.style.spacing.icon_spacing, } } }