mirror of
https://github.com/emilk/egui.git
synced 2026-09-02 23:00:04 -04:00
StyleProvider for TextEdit and DragValue (#8455)
Implements `StyleProvider` for `TextEdit`, so you can fully style it as you please --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -516,8 +516,6 @@ pub(crate) struct Focus {
|
|||||||
/// The ID of a widget to give the focus to in the next frame.
|
/// The ID of a widget to give the focus to in the next frame.
|
||||||
id_next_frame: Option<Id>,
|
id_next_frame: Option<Id>,
|
||||||
|
|
||||||
id_requested_by_accesskit: Option<accesskit::NodeId>,
|
|
||||||
|
|
||||||
/// If set, the next widget that is interested in focus will automatically get it.
|
/// If set, the next widget that is interested in focus will automatically get it.
|
||||||
/// Probably because the user pressed Tab.
|
/// Probably because the user pressed Tab.
|
||||||
give_to_next: bool,
|
give_to_next: bool,
|
||||||
@@ -575,10 +573,10 @@ impl Focus {
|
|||||||
}
|
}
|
||||||
let event_filter = self.focused_widget.map(|w| w.filter).unwrap_or_default();
|
let event_filter = self.focused_widget.map(|w| w.filter).unwrap_or_default();
|
||||||
|
|
||||||
self.id_requested_by_accesskit = None;
|
|
||||||
|
|
||||||
self.focus_direction = FocusDirection::None;
|
self.focus_direction = FocusDirection::None;
|
||||||
|
|
||||||
|
let mut focus_requested_by_accesskit = None;
|
||||||
|
|
||||||
for event in &new_input.events {
|
for event in &new_input.events {
|
||||||
if !event_filter.matches(event)
|
if !event_filter.matches(event)
|
||||||
&& let crate::Event::Key {
|
&& let crate::Event::Key {
|
||||||
@@ -615,9 +613,22 @@ impl Focus {
|
|||||||
}) = event
|
}) = event
|
||||||
&& *target_tree == accesskit::TreeId::ROOT
|
&& *target_tree == accesskit::TreeId::ROOT
|
||||||
{
|
{
|
||||||
self.id_requested_by_accesskit = Some(*target_node);
|
focus_requested_by_accesskit = Some(*target_node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle accesskit focus requests
|
||||||
|
let newly_focused = focus_requested_by_accesskit.and_then(|node_id| {
|
||||||
|
self.focus_widgets_cache
|
||||||
|
.keys()
|
||||||
|
.find(|id| id.accesskit_id() == node_id)
|
||||||
|
.copied()
|
||||||
|
});
|
||||||
|
if let Some(id) = newly_focused {
|
||||||
|
self.focused_widget = Some(FocusWidget::new(id));
|
||||||
|
self.give_to_next = false;
|
||||||
|
self.reset_focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn end_pass(&mut self, used_ids: &IdMap<Rect>) {
|
pub(crate) fn end_pass(&mut self, used_ids: &IdMap<Rect>) {
|
||||||
@@ -645,13 +656,6 @@ impl Focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn interested_in_focus(&mut self, id: Id) {
|
fn interested_in_focus(&mut self, id: Id) {
|
||||||
if self.id_requested_by_accesskit == Some(id.accesskit_id()) {
|
|
||||||
self.focused_widget = Some(FocusWidget::new(id));
|
|
||||||
self.id_requested_by_accesskit = None;
|
|
||||||
self.give_to_next = false;
|
|
||||||
self.reset_focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The rect is updated at the end of the frame.
|
// The rect is updated at the end of the frame.
|
||||||
self.focus_widgets_cache
|
self.focus_widgets_cache
|
||||||
.entry(id)
|
.entry(id)
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
use emath::Vec2;
|
use emath::Vec2;
|
||||||
use epaint::Margin;
|
use epaint::{Color32, Margin};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Button, Context, Frame, TextStyle,
|
Button, Context, Frame, TextEdit, TextStyle,
|
||||||
class::HasClasses as _,
|
class::HasClasses as _,
|
||||||
theme::StyleProvider,
|
theme::StyleProvider,
|
||||||
widget_style::{
|
widget_style::{
|
||||||
AtomLayoutStyle, ButtonStyle, CheckboxStyle, SeparatorStyle, StyleArgs, TextVisuals,
|
AtomLayoutStyle, ButtonStyle, CheckboxStyle, SeparatorStyle, StyleArgs, TextEditStyle,
|
||||||
WidgetState,
|
TextVisuals, WidgetState,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -25,6 +25,7 @@ impl DefaultStyle {
|
|||||||
ctx.add_widget_theme::<ButtonStyle>(Self);
|
ctx.add_widget_theme::<ButtonStyle>(Self);
|
||||||
ctx.add_widget_theme::<SeparatorStyle>(Self);
|
ctx.add_widget_theme::<SeparatorStyle>(Self);
|
||||||
ctx.add_widget_theme::<CheckboxStyle>(Self);
|
ctx.add_widget_theme::<CheckboxStyle>(Self);
|
||||||
|
ctx.add_widget_theme::<TextEditStyle>(Self);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +86,7 @@ impl StyleProvider<ButtonStyle> for DefaultStyle {
|
|||||||
let image_tint = if classes.has_class(&Button::CLASS_IMAGE_TINT_FOLLOWS_TEXT_COLOR) {
|
let image_tint = if classes.has_class(&Button::CLASS_IMAGE_TINT_FOLLOWS_TEXT_COLOR) {
|
||||||
text_style.color
|
text_style.color
|
||||||
} else {
|
} else {
|
||||||
crate::Color32::WHITE
|
Color32::WHITE
|
||||||
};
|
};
|
||||||
|
|
||||||
ButtonStyle {
|
ButtonStyle {
|
||||||
@@ -105,6 +106,62 @@ impl StyleProvider<ButtonStyle> for DefaultStyle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl StyleProvider<TextEditStyle> for DefaultStyle {
|
||||||
|
fn style(&mut self, modifiers: &StyleArgs<'_>) -> TextEditStyle {
|
||||||
|
let StyleArgs {
|
||||||
|
classes,
|
||||||
|
style,
|
||||||
|
state,
|
||||||
|
..
|
||||||
|
} = modifiers;
|
||||||
|
|
||||||
|
let widget_visuals = style.visuals.widgets.state(*state);
|
||||||
|
|
||||||
|
// A text edit over an immutable buffer is painted without a background.
|
||||||
|
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.selection.stroke
|
||||||
|
} else {
|
||||||
|
widget_visuals.bg_stroke
|
||||||
|
};
|
||||||
|
|
||||||
|
// The text of a text edit doesn't brighten on hover — that would be distracting while
|
||||||
|
// typing — so it keeps the inactive color no matter the state.
|
||||||
|
let text = TextVisuals::from_widget_visuals(
|
||||||
|
style,
|
||||||
|
TextStyle::Body,
|
||||||
|
&style.visuals.widgets.inactive,
|
||||||
|
);
|
||||||
|
|
||||||
|
TextEditStyle {
|
||||||
|
atom_layout: AtomLayoutStyle {
|
||||||
|
frame: Frame {
|
||||||
|
fill,
|
||||||
|
corner_radius: widget_visuals.corner_radius,
|
||||||
|
inner_margin: Margin::symmetric(4, 2),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.apply_stroke_and_expansion_without_layout_shift(stroke, 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl StyleProvider<CheckboxStyle> for DefaultStyle {
|
impl StyleProvider<CheckboxStyle> for DefaultStyle {
|
||||||
fn style(&mut self, modifiers: &StyleArgs<'_>) -> CheckboxStyle {
|
fn style(&mut self, modifiers: &StyleArgs<'_>) -> CheckboxStyle {
|
||||||
let StyleArgs { style, state, .. } = modifiers;
|
let StyleArgs { style, state, .. } = modifiers;
|
||||||
|
|||||||
@@ -133,6 +133,24 @@ pub struct ButtonStyle {
|
|||||||
|
|
||||||
impl WidgetStyle for ButtonStyle {}
|
impl WidgetStyle for ButtonStyle {}
|
||||||
|
|
||||||
|
/// Dedicated text edit style
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TextEditStyle {
|
||||||
|
/// Style of the field's atom layout.
|
||||||
|
///
|
||||||
|
/// [`AtomLayoutStyle::frame`] surrounds the text, including its padding, and
|
||||||
|
/// [`AtomLayoutStyle::text_style`] is the text being edited.
|
||||||
|
pub atom_layout: AtomLayoutStyle,
|
||||||
|
|
||||||
|
/// 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 {}
|
||||||
|
|
||||||
/// Dedicated checkbox style
|
/// Dedicated checkbox style
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct CheckboxStyle {
|
pub struct CheckboxStyle {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Atom, AtomExt as _, AtomKind, Atoms, Button, CursorIcon, Id, IntoAtoms, Key, MINUS_CHAR_STR,
|
Atom, AtomExt as _, AtomKind, Atoms, Button, CursorIcon, Id, IntoAtoms, Key, MINUS_CHAR_STR,
|
||||||
Modifiers, NumExt as _, Response, RichText, Sense, TextEdit, TextWrapMode, Ui, Widget,
|
Modifiers, NumExt as _, Response, RichText, Sense, TextEdit, TextWrapMode, Ui, Widget,
|
||||||
WidgetInfo, emath, text,
|
WidgetInfo,
|
||||||
|
class::{ClassName, Classes, HasClasses},
|
||||||
|
emath, text,
|
||||||
};
|
};
|
||||||
use core::{cmp::Ordering, ops::RangeInclusive};
|
use core::{cmp::Ordering, ops::RangeInclusive};
|
||||||
use emath::Vec2;
|
use emath::Vec2;
|
||||||
@@ -63,11 +65,16 @@ pub struct DragValue<'a> {
|
|||||||
custom_formatter: Option<NumFormatter<'a>>,
|
custom_formatter: Option<NumFormatter<'a>>,
|
||||||
custom_parser: Option<NumParser<'a>>,
|
custom_parser: Option<NumParser<'a>>,
|
||||||
update_while_editing: bool,
|
update_while_editing: bool,
|
||||||
|
classes: Classes,
|
||||||
|
min_size: Option<Vec2>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> DragValue<'a> {
|
impl<'a> DragValue<'a> {
|
||||||
const ATOM_ID: &'static str = "drag_item";
|
const ATOM_ID: &'static str = "drag_item";
|
||||||
|
|
||||||
|
/// Present on the [`Button`] and the [`TextEdit`] a drag value is built from.
|
||||||
|
pub const CLASS: ClassName = ClassName::from_static("egui::drag_value");
|
||||||
|
|
||||||
pub fn new<Num: emath::Numeric>(value: &'a mut Num) -> Self {
|
pub fn new<Num: emath::Numeric>(value: &'a mut Num) -> Self {
|
||||||
let slf = Self::from_get_set(move |v: Option<f64>| {
|
let slf = Self::from_get_set(move |v: Option<f64>| {
|
||||||
if let Some(v) = v {
|
if let Some(v) = v {
|
||||||
@@ -97,9 +104,20 @@ impl<'a> DragValue<'a> {
|
|||||||
custom_formatter: None,
|
custom_formatter: None,
|
||||||
custom_parser: None,
|
custom_parser: None,
|
||||||
update_while_editing: true,
|
update_while_editing: true,
|
||||||
|
classes: Classes::default().with_class(Self::CLASS),
|
||||||
|
min_size: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The minimum size of the drag value, in both its dragged and its text-edited state.
|
||||||
|
///
|
||||||
|
/// Defaults to [`crate::style::Spacing::interact_size`].
|
||||||
|
#[inline]
|
||||||
|
pub fn min_size(mut self, min_size: Vec2) -> Self {
|
||||||
|
self.min_size = Some(min_size);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// How much the value changes when dragged one point (logical pixel).
|
/// How much the value changes when dragged one point (logical pixel).
|
||||||
///
|
///
|
||||||
/// Should be finite and greater than zero.
|
/// Should be finite and greater than zero.
|
||||||
@@ -449,6 +467,8 @@ impl Widget for DragValue<'_> {
|
|||||||
custom_formatter,
|
custom_formatter,
|
||||||
custom_parser,
|
custom_parser,
|
||||||
update_while_editing,
|
update_while_editing,
|
||||||
|
classes,
|
||||||
|
min_size,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let mut prefix_text = String::new();
|
let mut prefix_text = String::new();
|
||||||
@@ -583,11 +603,11 @@ impl Widget for DragValue<'_> {
|
|||||||
.map_or_else(|| value_text.clone(), |edit_state| edit_state.text);
|
.map_or_else(|| value_text.clone(), |edit_state| edit_state.text);
|
||||||
let response = ui.add(
|
let response = ui.add(
|
||||||
TextEdit::singleline(&mut value_text)
|
TextEdit::singleline(&mut value_text)
|
||||||
|
.with_classes(classes)
|
||||||
.clip_text(false)
|
.clip_text(false)
|
||||||
.horizontal_align(ui.layout().horizontal_align())
|
.horizontal_align(ui.layout().horizontal_align())
|
||||||
.vertical_align(ui.layout().vertical_align())
|
.vertical_align(ui.layout().vertical_align())
|
||||||
.margin(ui.spacing().button_padding)
|
.min_size(min_size.unwrap_or_else(|| ui.spacing().interact_size))
|
||||||
.min_size(ui.spacing().interact_size)
|
|
||||||
.id(id)
|
.id(id)
|
||||||
.desired_width(
|
.desired_width(
|
||||||
ui.spacing().interact_size.x - 2.0 * ui.spacing().button_padding.x,
|
ui.spacing().interact_size.x - 2.0 * ui.spacing().button_padding.x,
|
||||||
@@ -634,10 +654,11 @@ impl Widget for DragValue<'_> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
let button = Button::new(atoms)
|
let button = Button::new(atoms)
|
||||||
|
.with_classes(classes)
|
||||||
.wrap_mode(TextWrapMode::Extend)
|
.wrap_mode(TextWrapMode::Extend)
|
||||||
.sense(Sense::click_and_drag())
|
.sense(Sense::click_and_drag())
|
||||||
.gap(0.0)
|
.gap(0.0)
|
||||||
.min_size(ui.spacing().interact_size); // TODO(emilk): find some more generic solution to `min_size`
|
.min_size(min_size.unwrap_or_else(|| ui.spacing().interact_size)); // TODO(emilk): find some more generic solution to `min_size`
|
||||||
|
|
||||||
let cursor_icon = if value <= *range.start() {
|
let cursor_icon = if value <= *range.start() {
|
||||||
CursorIcon::ResizeEast
|
CursorIcon::ResizeEast
|
||||||
@@ -805,6 +826,16 @@ fn select_all_text(ui: &Ui, widget_id: Id, response_id: Id, value_text: &str) {
|
|||||||
state.store(ui.ctx(), response_id);
|
state.store(ui.ctx(), response_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HasClasses for DragValue<'_> {
|
||||||
|
fn classes(&self) -> &Classes {
|
||||||
|
&self.classes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn classes_mut(&mut self) -> &mut Classes {
|
||||||
|
&mut self.classes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::clamp_value_to_range;
|
use super::clamp_value_to_range;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use crate::{
|
|||||||
CursorIcon, Event, EventFilter, FontSelection, Frame, IMEPurpose, Id, IdSalt, ImeEvent,
|
CursorIcon, Event, EventFilter, FontSelection, Frame, IMEPurpose, Id, IdSalt, ImeEvent,
|
||||||
IntoAtoms, IntoSizedResult, Key, KeyboardShortcut, Margin, Modifiers, NumExt as _, Response,
|
IntoAtoms, IntoSizedResult, Key, KeyboardShortcut, Margin, Modifiers, NumExt as _, Response,
|
||||||
Sense, SizedAtomKind, TextBuffer, TextStyle, Ui, Vec2, Widget, WidgetInfo, WidgetWithState,
|
Sense, SizedAtomKind, TextBuffer, TextStyle, Ui, Vec2, Widget, WidgetInfo, WidgetWithState,
|
||||||
|
class::{ClassName, Classes, HasClasses},
|
||||||
epaint,
|
epaint,
|
||||||
os::OperatingSystem,
|
os::OperatingSystem,
|
||||||
output::OutputEvent,
|
output::OutputEvent,
|
||||||
@@ -17,6 +18,7 @@ use crate::{
|
|||||||
self, CCursorRange, text_cursor_state::cursor_rect, visuals::paint_text_selection,
|
self, CCursorRange, text_cursor_state::cursor_rect, visuals::paint_text_selection,
|
||||||
},
|
},
|
||||||
vec2,
|
vec2,
|
||||||
|
widget_style::TextEditStyle,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{TextEditOutput, TextEditState};
|
use super::{TextEditOutput, TextEditState};
|
||||||
@@ -79,7 +81,7 @@ pub struct TextEdit<'t> {
|
|||||||
layouter: Option<LayouterFn<'t>>,
|
layouter: Option<LayouterFn<'t>>,
|
||||||
password: bool,
|
password: bool,
|
||||||
frame: Option<Frame>,
|
frame: Option<Frame>,
|
||||||
margin: Margin,
|
margin: Option<Margin>,
|
||||||
multiline: bool,
|
multiline: bool,
|
||||||
interactive: bool,
|
interactive: bool,
|
||||||
desired_width: Option<f32>,
|
desired_width: Option<f32>,
|
||||||
@@ -92,6 +94,7 @@ pub struct TextEdit<'t> {
|
|||||||
char_limit: usize,
|
char_limit: usize,
|
||||||
return_key: Option<KeyboardShortcut>,
|
return_key: Option<KeyboardShortcut>,
|
||||||
background_color: Option<Color32>,
|
background_color: Option<Color32>,
|
||||||
|
classes: Classes,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WidgetWithState for TextEdit<'_> {
|
impl WidgetWithState for TextEdit<'_> {
|
||||||
@@ -99,6 +102,9 @@ impl WidgetWithState for TextEdit<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TextEdit<'_> {
|
impl TextEdit<'_> {
|
||||||
|
/// Present on a text edit whose buffer can't be edited.
|
||||||
|
pub const CLASS_READ_ONLY: ClassName = ClassName::from_static("egui::text_edit::read_only");
|
||||||
|
|
||||||
pub fn load_state(ctx: &Context, id: Id) -> Option<TextEditState> {
|
pub fn load_state(ctx: &Context, id: Id) -> Option<TextEditState> {
|
||||||
TextEditState::load(ctx, id)
|
TextEditState::load(ctx, id)
|
||||||
}
|
}
|
||||||
@@ -133,7 +139,7 @@ impl<'t> TextEdit<'t> {
|
|||||||
layouter: None,
|
layouter: None,
|
||||||
password: false,
|
password: false,
|
||||||
frame: None,
|
frame: None,
|
||||||
margin: Margin::symmetric(4, 2),
|
margin: None,
|
||||||
multiline: true,
|
multiline: true,
|
||||||
interactive: true,
|
interactive: true,
|
||||||
desired_width: None,
|
desired_width: None,
|
||||||
@@ -152,6 +158,7 @@ impl<'t> TextEdit<'t> {
|
|||||||
char_limit: usize::MAX,
|
char_limit: usize::MAX,
|
||||||
return_key: Some(KeyboardShortcut::new(Modifiers::NONE, Key::Enter)),
|
return_key: Some(KeyboardShortcut::new(Modifiers::NONE, Key::Enter)),
|
||||||
background_color: None,
|
background_color: None,
|
||||||
|
classes: Classes::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,10 +315,10 @@ impl<'t> TextEdit<'t> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set margin of text. Default is `Margin::symmetric(4.0, 2.0)`
|
/// Override the padding around the text, which otherwise comes from the theme.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn margin(mut self, margin: impl Into<Margin>) -> Self {
|
pub fn margin(mut self, margin: impl Into<Margin>) -> Self {
|
||||||
self.margin = margin.into();
|
self.margin = Some(margin.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,6 +424,16 @@ impl Widget for TextEdit<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HasClasses for TextEdit<'_> {
|
||||||
|
fn classes(&self) -> &Classes {
|
||||||
|
&self.classes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn classes_mut(&mut self) -> &mut Classes {
|
||||||
|
&mut self.classes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TextEdit<'_> {
|
impl TextEdit<'_> {
|
||||||
/// Show the [`TextEdit`], returning a rich [`TextEditOutput`].
|
/// Show the [`TextEdit`], returning a rich [`TextEditOutput`].
|
||||||
///
|
///
|
||||||
@@ -459,17 +476,42 @@ impl TextEdit<'_> {
|
|||||||
char_limit,
|
char_limit,
|
||||||
return_key,
|
return_key,
|
||||||
background_color,
|
background_color,
|
||||||
|
mut classes,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
|
let id = id.unwrap_or_else(|| {
|
||||||
|
if let Some(id_salt) = id_salt {
|
||||||
|
ui.make_persistent_id(id_salt)
|
||||||
|
} else {
|
||||||
|
// Since we are only storing the cursor a persistent Id is not super important
|
||||||
|
let id = ui.next_auto_id();
|
||||||
|
ui.skip_ahead_auto_ids(1);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
classes.add_class_if(Self::CLASS_READ_ONLY, !text.is_mutable());
|
||||||
|
let TextEditStyle {
|
||||||
|
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.at_least(atom_layout_style.min_size);
|
||||||
|
|
||||||
let text_color = text_color
|
let text_color = text_color
|
||||||
.or_else(|| ui.visuals().override_text_color)
|
.or_else(|| ui.visuals().override_text_color)
|
||||||
// .unwrap_or_else(|| ui.style().interact(&response).text_color()); // too bright
|
.unwrap_or(atom_layout_style.text_style.color);
|
||||||
.unwrap_or_else(|| ui.visuals().widgets.inactive.text_color());
|
|
||||||
|
|
||||||
let prev_text = text.as_str().to_owned();
|
let prev_text = text.as_str().to_owned();
|
||||||
let hint_text_str = hint_text.text().unwrap_or_default().to_string();
|
let hint_text_str = hint_text.text().unwrap_or_default().to_string();
|
||||||
|
|
||||||
let font_id = font_selection.resolve(ui.style());
|
let font_id = match font_selection {
|
||||||
|
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));
|
let row_height = ui.fonts_mut(|f| f.row_height(&font_id));
|
||||||
let line_height = row_height + ui.spacing().extra_text_line_spacing;
|
let line_height = row_height + ui.spacing().extra_text_line_spacing;
|
||||||
|
|
||||||
@@ -481,13 +523,12 @@ impl TextEdit<'_> {
|
|||||||
// `min_size` overrides available width
|
// `min_size` overrides available width
|
||||||
let allocate_width = desired_width.at_most(available_width).at_least(min_size.x);
|
let allocate_width = desired_width.at_most(available_width).at_least(min_size.x);
|
||||||
|
|
||||||
let font_id_clone = font_id.clone();
|
|
||||||
let mut default_layouter = move |ui: &Ui, text: &dyn TextBuffer, wrap_width: f32| {
|
let mut default_layouter = move |ui: &Ui, text: &dyn TextBuffer, wrap_width: f32| {
|
||||||
let text = mask_if_password(password, text.as_str());
|
let text = mask_if_password(password, text.as_str());
|
||||||
let mut layout_job = if multiline {
|
let mut layout_job = if multiline {
|
||||||
LayoutJob::simple(text, font_id_clone.clone(), text_color, wrap_width)
|
LayoutJob::simple(text, font_id.clone(), text_color, wrap_width)
|
||||||
} else {
|
} else {
|
||||||
LayoutJob::simple_singleline(text, font_id_clone.clone(), text_color)
|
LayoutJob::simple_singleline(text, font_id.clone(), text_color)
|
||||||
};
|
};
|
||||||
layout_job.halign = align.x();
|
layout_job.halign = align.x();
|
||||||
// We want to keep the trailing whitespace, since hiding it feels really weird when typing
|
// We want to keep the trailing whitespace, since hiding it feels really weird when typing
|
||||||
@@ -504,17 +545,6 @@ impl TextEdit<'_> {
|
|||||||
|
|
||||||
let min_inner_height = (desired_height_rows.at_least(1) as f32) * line_height;
|
let min_inner_height = (desired_height_rows.at_least(1) as f32) * line_height;
|
||||||
|
|
||||||
let id = id.unwrap_or_else(|| {
|
|
||||||
if let Some(id_salt) = id_salt {
|
|
||||||
ui.make_persistent_id(id_salt)
|
|
||||||
} else {
|
|
||||||
// Since we are only storing the cursor a persistent Id is not super important
|
|
||||||
let id = ui.next_auto_id();
|
|
||||||
ui.skip_ahead_auto_ids(1);
|
|
||||||
id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// On touch screens (e.g. mobile in `eframe` web), should
|
// On touch screens (e.g. mobile in `eframe` web), should
|
||||||
// dragging select text, or scroll the enclosing [`ScrollArea`] (if any)?
|
// dragging select text, or scroll the enclosing [`ScrollArea`] (if any)?
|
||||||
// Since currently copying selected text in not supported on `eframe` web,
|
// Since currently copying selected text in not supported on `eframe` web,
|
||||||
@@ -550,7 +580,6 @@ impl TextEdit<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut text_changed = false;
|
let mut text_changed = false;
|
||||||
let text_mutable = text.is_mutable();
|
|
||||||
|
|
||||||
let mut handle_events = |ui: &Ui, galley: &mut Arc<Galley>, layouter, wrap_width, text| {
|
let mut handle_events = |ui: &Ui, galley: &mut Arc<Galley>, layouter, wrap_width, text| {
|
||||||
if interactive && ui.memory(|mem| mem.has_focus(id)) {
|
if interactive && ui.memory(|mem| mem.has_focus(id)) {
|
||||||
@@ -592,6 +621,18 @@ impl TextEdit<'_> {
|
|||||||
// We need to calculate the galley within the atom closure, so we can calculate it based on
|
// 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,
|
// 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.
|
// so we can clip it to the available size. Thus, extract it from the atom closure here.
|
||||||
|
if let Some(frame) = frame {
|
||||||
|
atom_layout_style.frame = frame;
|
||||||
|
} else {
|
||||||
|
if let Some(margin) = margin {
|
||||||
|
atom_layout_style.frame.inner_margin = margin;
|
||||||
|
}
|
||||||
|
if let Some(background_color) = background_color {
|
||||||
|
atom_layout_style.frame.fill = background_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let frame = atom_layout_style.frame;
|
||||||
|
|
||||||
let mut get_galley = None;
|
let mut get_galley = None;
|
||||||
let inner_rect_id = Id::new("text_edit_rect");
|
let inner_rect_id = Id::new("text_edit_rect");
|
||||||
let mut response = {
|
let mut response = {
|
||||||
@@ -613,7 +654,7 @@ impl TextEdit<'_> {
|
|||||||
|
|
||||||
// Since we can't set a fallback color per atom, we have to override it here.
|
// Since we can't set a fallback color per atom, we have to override it here.
|
||||||
// Sucks, since it means users won't be able to override it.
|
// Sucks, since it means users won't be able to override it.
|
||||||
hint_text.map_texts(|t| t.color(ui.style().visuals.weak_text_color()));
|
hint_text.map_texts(|t| t.color(hint_text_color));
|
||||||
|
|
||||||
for mut atom in hint_text {
|
for mut atom in hint_text {
|
||||||
if !shrunk && matches!(atom.kind, AtomKind::Text(_)) {
|
if !shrunk && matches!(atom.kind, AtomKind::Text(_)) {
|
||||||
@@ -638,7 +679,7 @@ impl TextEdit<'_> {
|
|||||||
|
|
||||||
// Calculate the empty galley, so it can be read later. The available width is
|
// Calculate the empty galley, so it can be read later. The available width is
|
||||||
// technically wrong, but doesn't matter since the galley is empty
|
// technically wrong, but doesn't matter since the galley is empty
|
||||||
let available_width = allocate_width - margin.sum().x;
|
let available_width = allocate_width - frame.total_margin().sum().x;
|
||||||
let galley = layouter(ui, text, available_width);
|
let galley = layouter(ui, text, available_width);
|
||||||
|
|
||||||
// We can't update the galley immediately here, since it would show both hint text
|
// We can't update the galley immediately here, since it would show both hint text
|
||||||
@@ -693,9 +734,6 @@ impl TextEdit<'_> {
|
|||||||
atoms.push_right(atom);
|
atoms.push_right(atom);
|
||||||
}
|
}
|
||||||
|
|
||||||
let custom_frame = frame.is_some();
|
|
||||||
let frame = frame.unwrap_or_else(|| Frame::new().inner_margin(margin));
|
|
||||||
|
|
||||||
let min_height = (min_inner_height + frame.total_margin().sum().y).at_least(min_size.y);
|
let min_height = (min_inner_height + frame.total_margin().sum().y).at_least(min_size.y);
|
||||||
|
|
||||||
// This wrap mode only affects the hint_text
|
// This wrap mode only affects the hint_text
|
||||||
@@ -705,53 +743,19 @@ impl TextEdit<'_> {
|
|||||||
TextWrapMode::Truncate
|
TextWrapMode::Truncate
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut 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)
|
.id(id)
|
||||||
.min_size(Vec2::new(allocate_width, min_height))
|
.min_size(Vec2::new(allocate_width, min_height.at_least(min_size.y)))
|
||||||
.max_width(allocate_width)
|
.max_width(allocate_width)
|
||||||
.sense(sense)
|
.sense(sense)
|
||||||
.frame(frame)
|
|
||||||
.align2(align)
|
.align2(align)
|
||||||
.wrap_mode(wrap_mode)
|
.wrap_mode(wrap_mode)
|
||||||
.allocate(ui);
|
.allocate(ui);
|
||||||
|
|
||||||
allocated.frame = if custom_frame {
|
|
||||||
allocated.frame
|
|
||||||
} else {
|
|
||||||
let visuals = ui.style().interact(&allocated.response);
|
|
||||||
let background_color =
|
|
||||||
background_color.unwrap_or_else(|| ui.visuals().text_edit_bg_color());
|
|
||||||
|
|
||||||
let (corner_radius, background_color, stroke) = if text_mutable {
|
|
||||||
if allocated.response.has_focus() {
|
|
||||||
(
|
|
||||||
visuals.corner_radius,
|
|
||||||
background_color,
|
|
||||||
ui.visuals().selection.stroke,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(visuals.corner_radius, background_color, visuals.bg_stroke)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let visuals = &ui.style().visuals.widgets.inactive;
|
|
||||||
(
|
|
||||||
visuals.corner_radius,
|
|
||||||
Color32::TRANSPARENT,
|
|
||||||
visuals.bg_stroke,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
allocated
|
|
||||||
.frame
|
|
||||||
.fill(background_color)
|
|
||||||
.corner_radius(corner_radius)
|
|
||||||
.inner_margin(
|
|
||||||
allocated.frame.inner_margin
|
|
||||||
+ Margin::same((visuals.expansion - stroke.width).round() as i8),
|
|
||||||
)
|
|
||||||
.outer_margin(Margin::same(-(visuals.expansion as i8)))
|
|
||||||
.stroke(stroke)
|
|
||||||
};
|
|
||||||
|
|
||||||
allocated.paint(ui)
|
allocated.paint(ui)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:570216d6ffa3cc278705582d95b96200bc3ef1608b8f2983a6ed3f8b2ee7276c
|
oid sha256:806f3f69228e62d9d0838fb27a02f46cae29efffee54d8182ecbfd2c12f405ba
|
||||||
size 2280
|
size 2413
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b66afca19d0a0a6fac4737816443fbd481635897807689dbd75321360ffa079d
|
oid sha256:2ae71de200dfb36f64e1ce95c2ad78ef3c1e88e792ada5b5fa1d8fa8c938d8fd
|
||||||
size 13803
|
size 13806
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:20afbde3e1edc7a599936f83188fb1a2c35d2d1344ce7ad8e115b937266481fc
|
oid sha256:10459c5ac931ee55d814e7e4122167f1d4050fff39c409cef995c91d6deaef55
|
||||||
size 2547
|
size 2563
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4b0af10fca47b33752aed2549528dba6b6e8c4818f2fc2b9de60451afdd30a2b
|
oid sha256:123f7d1a86d644d2e830a2c8b73e8c509ce33fd5d3ead28d3eccf6180a2fcfc5
|
||||||
size 1690
|
size 1790
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:65161f878e9f6609f8b8a8898ad6c11ef3a730682389b6ea5e3fb6534277cb34
|
oid sha256:73c293f63b9b7f02fd0f3a506dd076eb1e653b7fbb7554603e457b9ad2552f48
|
||||||
size 9247
|
size 9276
|
||||||
|
|||||||
Reference in New Issue
Block a user