mirror of
https://github.com/emilk/egui.git
synced 2026-08-31 13:50:04 -04:00
Let the theme style a TextEdit's prefix and suffix
`TextEdit` unpacked `AtomLayoutStyle` and re-applied the pieces by hand, which silently dropped `align2` and `image_tint` and made a per-widget gap lose to the theme's. Go through `AtomLayoutStyle::apply`, like `Button` and `Checkbox` do, and add a `prefix_suffix_color` so the atoms around the input keep the general text color rather than the text edit's own. Tidy the provider while here: use `Widgets::state`, split the read-only `fill`/`stroke` decision in two, and take the expansion correction from `Frame::expand_in_place`. Drop `TextVisuals::from_style`, `UiStack::inherited`, `StyleArgs::inherited` and `HasClasses::last_class`, none of which have callers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use emath::Vec2;
|
||||
use epaint::Margin;
|
||||
use epaint::{Color32, Margin};
|
||||
|
||||
use crate::{
|
||||
Button, Context, Frame, TextEdit, TextStyle,
|
||||
@@ -114,25 +114,24 @@ impl StyleProvider<TextEditStyle> for DefaultStyle {
|
||||
..
|
||||
} = modifiers;
|
||||
|
||||
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 widget_visuals = style.visuals.widgets.state(*state);
|
||||
|
||||
// A text edit over an immutable buffer is painted without a background.
|
||||
let (fill, stroke) = if classes.has_class(TextEdit::CLASS_READ_ONLY) {
|
||||
let visuals = &style.visuals.widgets.inactive;
|
||||
(Color32::TRANSPARENT, visuals.bg_stroke)
|
||||
let read_only = classes.has_class(TextEdit::CLASS_READ_ONLY);
|
||||
|
||||
let fill = if read_only {
|
||||
Color32::TRANSPARENT
|
||||
} else {
|
||||
style.visuals.text_edit_bg_color()
|
||||
};
|
||||
|
||||
let stroke = if read_only {
|
||||
style.visuals.widgets.inactive.bg_stroke
|
||||
} else if *state == WidgetState::Active {
|
||||
// While focused, the frame is outlined in the selection color.
|
||||
(
|
||||
style.visuals.text_edit_bg_color(),
|
||||
style.visuals.selection.stroke,
|
||||
)
|
||||
style.visuals.selection.stroke
|
||||
} else {
|
||||
(style.visuals.text_edit_bg_color(), widget_visuals.bg_stroke)
|
||||
widget_visuals.bg_stroke
|
||||
};
|
||||
|
||||
// The text of a text edit doesn't brighten on hover — that would be distracting while
|
||||
@@ -149,21 +148,16 @@ impl StyleProvider<TextEditStyle> for DefaultStyle {
|
||||
fill,
|
||||
stroke,
|
||||
corner_radius: widget_visuals.corner_radius,
|
||||
// The stroke is painted centered on the frame edge, so half of it eats into
|
||||
// the padding; compensate, like the other widgets do.
|
||||
inner_margin: Margin::symmetric(4, 2)
|
||||
+ Margin::same((widget_visuals.expansion - stroke.width).round() as i8),
|
||||
outer_margin: Margin::same(-(widget_visuals.expansion as i8)),
|
||||
inner_margin: Margin::symmetric(4, 2),
|
||||
..Default::default()
|
||||
},
|
||||
// A text edit sizes itself from the rows it holds; egui's own theme adds no floor
|
||||
// of its own.
|
||||
min_size: Vec2::ZERO,
|
||||
}
|
||||
.expand_in_place(widget_visuals.expansion),
|
||||
gap: style.spacing.icon_spacing,
|
||||
text_style: text,
|
||||
..Default::default()
|
||||
},
|
||||
hint_text_color: style.visuals.weak_text_color(),
|
||||
prefix_suffix_color: style.visuals.text_color(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,15 +297,6 @@ impl UiStack {
|
||||
self.iter()
|
||||
.any(|node| node.classes.has_class(class.clone()))
|
||||
}
|
||||
|
||||
/// Read a value from the classes up the stack, nearest node first.
|
||||
///
|
||||
/// This is how an inherited property works in a stylesheet: a container states it once, and
|
||||
/// everything inside picks it up. The nearest [`crate::Ui`] wins, so an inner container can
|
||||
/// override an outer one.
|
||||
pub fn inherited<T>(&self, from_classes: impl Fn(&Classes) -> Option<T>) -> Option<T> {
|
||||
self.iter().find_map(|node| from_classes(&node.classes))
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@@ -158,17 +158,6 @@ pub trait HasClasses {
|
||||
fn as_slice(&self) -> &[ClassName] {
|
||||
&self.classes().classes
|
||||
}
|
||||
|
||||
/// The last class that names a `T`, if any.
|
||||
///
|
||||
/// Scanning backwards is what makes the last class win, so a widget given two classes of the
|
||||
/// same kind takes the one set last — the way a later stylesheet rule overrides an earlier one.
|
||||
fn last_class<T>(&self, from_class_name: impl Fn(&str) -> Option<T>) -> Option<T> {
|
||||
self.as_slice()
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|class| from_class_name(class))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -147,6 +147,9 @@ pub struct TextEditStyle {
|
||||
|
||||
/// The color of the hint text shown while the buffer is empty.
|
||||
pub hint_text_color: Color32,
|
||||
|
||||
/// The default color of the prefix and suffix atoms.
|
||||
pub prefix_suffix_color: Color32,
|
||||
}
|
||||
|
||||
impl WidgetStyle for TextEditStyle {}
|
||||
@@ -238,11 +241,4 @@ impl StyleArgs<'_> {
|
||||
let class = class.into();
|
||||
self.classes.has_class(class.clone()) || self.stack.has_class(class)
|
||||
}
|
||||
|
||||
/// Read a value from the widget's own classes, falling back to the [`crate::Ui`]s it sits in.
|
||||
///
|
||||
/// See [`UiStack::inherited`] for how the fallback resolves.
|
||||
pub fn inherited<T>(&self, from_classes: impl Fn(&Classes) -> Option<T>) -> Option<T> {
|
||||
from_classes(self.classes).or_else(|| self.stack.inherited(from_classes))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::{
|
||||
self, CCursorRange, text_cursor_state::cursor_rect, visuals::paint_text_selection,
|
||||
},
|
||||
vec2,
|
||||
widget_style::{AtomLayoutStyle, Classes, HasClasses, TextEditStyle},
|
||||
widget_style::{Classes, HasClasses, TextEditStyle},
|
||||
};
|
||||
|
||||
use super::{TextEditOutput, TextEditState};
|
||||
@@ -491,30 +491,24 @@ impl TextEdit<'_> {
|
||||
|
||||
classes.add_class_if(Self::CLASS_READ_ONLY, !text.is_mutable());
|
||||
let TextEditStyle {
|
||||
atom_layout:
|
||||
AtomLayoutStyle {
|
||||
frame: styled_frame,
|
||||
min_size: style_min_size,
|
||||
gap,
|
||||
text_style: text_visuals,
|
||||
..
|
||||
},
|
||||
atom_layout: mut atom_layout_style,
|
||||
hint_text_color,
|
||||
prefix_suffix_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 min_size = min_size.at_least(atom_layout_style.min_size);
|
||||
|
||||
let text_color = text_color
|
||||
.or_else(|| ui.visuals().override_text_color)
|
||||
.unwrap_or(text_visuals.color);
|
||||
.unwrap_or(atom_layout_style.text_style.color);
|
||||
|
||||
let prev_text = text.as_str().to_owned();
|
||||
let hint_text_str = hint_text.text().unwrap_or_default().to_string();
|
||||
|
||||
let font_id = match font_selection {
|
||||
FontSelection::Default => text_visuals.font_id.clone(),
|
||||
FontSelection::Default => atom_layout_style.text_style.font_id.clone(),
|
||||
font_selection => font_selection.resolve(ui.style()),
|
||||
};
|
||||
let row_height = ui.fonts_mut(|f| f.row_height(&font_id));
|
||||
@@ -527,7 +521,7 @@ impl TextEdit<'_> {
|
||||
.at_least(min_size.x);
|
||||
let allocate_width = desired_width.at_most(available_width);
|
||||
|
||||
let font_id_clone = font_id.clone();
|
||||
let font_id_clone = font_id;
|
||||
let mut default_layouter = move |ui: &Ui, text: &dyn TextBuffer, wrap_width: f32| {
|
||||
let text = mask_if_password(password, text.as_str());
|
||||
let mut layout_job = if multiline {
|
||||
@@ -626,16 +620,17 @@ impl TextEdit<'_> {
|
||||
// We need to calculate the galley within the atom closure, so we can calculate it based on
|
||||
// the available width (in case of wrapping multiline text edits). But we show it later,
|
||||
// so we can clip it to the available size. Thus, extract it from the atom closure here.
|
||||
let frame = frame.unwrap_or_else(|| {
|
||||
let mut frame = styled_frame;
|
||||
if let Some(frame) = frame {
|
||||
atom_layout_style.frame = frame;
|
||||
} else {
|
||||
if let Some(margin) = margin {
|
||||
frame.inner_margin = margin;
|
||||
atom_layout_style.frame.inner_margin = margin;
|
||||
}
|
||||
if let Some(background_color) = background_color {
|
||||
frame.fill = background_color;
|
||||
atom_layout_style.frame.fill = background_color;
|
||||
}
|
||||
frame
|
||||
});
|
||||
}
|
||||
let frame = atom_layout_style.frame;
|
||||
|
||||
let mut get_galley = None;
|
||||
let inner_rect_id = Id::new("text_edit_rect");
|
||||
@@ -747,13 +742,15 @@ impl TextEdit<'_> {
|
||||
TextWrapMode::Truncate
|
||||
};
|
||||
|
||||
let allocated = AtomLayout::new(atoms)
|
||||
let allocated = atom_layout_style
|
||||
.apply(AtomLayout::new(atoms))
|
||||
// The text being edited gets its color from the layouter, so the only atoms
|
||||
// left to color are the prefix and the suffix.
|
||||
.fallback_text_color(prefix_suffix_color)
|
||||
.id(id)
|
||||
.gap(gap)
|
||||
.min_size(Vec2::new(allocate_width, min_height.at_least(min_size.y)))
|
||||
.max_width(allocate_width)
|
||||
.sense(sense)
|
||||
.frame(frame)
|
||||
.align2(align)
|
||||
.wrap_mode(wrap_mode)
|
||||
.allocate(ui);
|
||||
|
||||
Reference in New Issue
Block a user