1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 14:49:06 -04:00

TextEdit Atom prefix/suffix (#7587)

* part of https://github.com/emilk/egui/issues/7264
* part of https://github.com/emilk/egui/issues/7445

This PR changes the layout within the TextEdit to be done with
AtomLayout. It also adds a Prefix and Postfix that allows adding
permanent icons / icon buttons within the textedit.

Breaking changes:
- Removed `TextEdit::hint_text_font`. Hint text is an atom now, so the
font can be set that way

<img width="264" height="130" alt="Screenshot 2025-10-06 at 12 25 21"
src="https://github.com/user-attachments/assets/03b6b56b-ca82-4ac3-b5c0-585cca336834"
/>

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
Co-authored-by: lucasmerlin <8009393+lucasmerlin@users.noreply.github.com>
This commit is contained in:
Lucas Meurer
2026-03-20 11:29:32 +01:00
committed by GitHub
parent ad510257de
commit 8b2315375b
31 changed files with 427 additions and 259 deletions

View File

@@ -203,7 +203,7 @@ impl<'a> AtomLayout<'a> {
// If the TextWrapMode is not Extend, ensure there is some item marked as `shrink`.
// If none is found, mark the first text item as `shrink`.
if wrap_mode != TextWrapMode::Extend {
let any_shrink = atoms.iter().any(|a| a.shrink);
let any_shrink = atoms.any_shrink();
if !any_shrink {
let first_text = atoms
.iter_mut()

View File

@@ -69,6 +69,11 @@ impl<'a> Atoms<'a> {
string
}
/// Do any of the atoms have shrink set to `true`?
pub fn any_shrink(&self) -> bool {
self.iter().any(|a| a.shrink)
}
pub fn iter_kinds(&self) -> impl Iterator<Item = &AtomKind<'a>> {
self.0.iter().map(|atom| &atom.kind)
}

View File

@@ -1,15 +1,13 @@
use std::sync::Arc;
use emath::{Rect, TSTransform};
use epaint::{
StrokeKind,
text::{Galley, LayoutJob, cursor::CCursor},
};
use epaint::text::{Galley, LayoutJob, TextWrapMode, cursor::CCursor};
use crate::{
Align, Align2, Color32, Context, CursorIcon, Event, EventFilter, FontSelection, Id, ImeEvent,
Key, KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, Shape, TextBuffer,
TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetWithState, epaint,
Align, Align2, Atom, AtomExt as _, AtomKind, AtomLayout, Atoms, Color32, Context, CursorIcon,
Event, EventFilter, FontSelection, Frame, Id, ImeEvent, IntoAtoms, IntoSizedResult, Key,
KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, SizedAtomKind, TextBuffer,
TextStyle, Ui, Vec2, Widget, WidgetInfo, WidgetWithState, epaint,
os::OperatingSystem,
output::OutputEvent,
response, text_selection,
@@ -67,15 +65,16 @@ type LayouterFn<'t> = &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc<Galley
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
pub struct TextEdit<'t> {
text: &'t mut dyn TextBuffer,
hint_text: WidgetText,
hint_text_font: Option<FontSelection>,
prefix: Atoms<'static>,
suffix: Atoms<'static>,
hint_text: Atoms<'static>,
id: Option<Id>,
id_salt: Option<Id>,
font_selection: FontSelection,
text_color: Option<Color32>,
layouter: Option<LayouterFn<'t>>,
password: bool,
frame: bool,
frame: Option<Frame>,
margin: Margin,
multiline: bool,
interactive: bool,
@@ -120,15 +119,16 @@ impl<'t> TextEdit<'t> {
pub fn multiline(text: &'t mut dyn TextBuffer) -> Self {
Self {
text,
prefix: Default::default(),
suffix: Default::default(),
hint_text: Default::default(),
hint_text_font: None,
id: None,
id_salt: None,
font_selection: Default::default(),
text_color: None,
layouter: None,
password: false,
frame: true,
frame: None,
margin: Margin::symmetric(4, 2),
multiline: true,
interactive: true,
@@ -202,8 +202,22 @@ impl<'t> TextEdit<'t> {
/// # });
/// ```
#[inline]
pub fn hint_text(mut self, hint_text: impl Into<WidgetText>) -> Self {
self.hint_text = hint_text.into();
pub fn hint_text(mut self, hint_text: impl IntoAtoms<'static>) -> Self {
self.hint_text = hint_text.into_atoms();
self
}
/// Add a prefix to the text edit. This will always be shown before the editable text.
#[inline]
pub fn prefix(mut self, prefix: impl IntoAtoms<'static>) -> Self {
self.prefix = prefix.into_atoms();
self
}
/// Add a suffix to the text edit. This will always be shown after the editable text.
#[inline]
pub fn suffix(mut self, suffix: impl IntoAtoms<'static>) -> Self {
self.suffix = suffix.into_atoms();
self
}
@@ -215,13 +229,6 @@ impl<'t> TextEdit<'t> {
self
}
/// Set a specific style for the hint text.
#[inline]
pub fn hint_text_font(mut self, hint_text_font: impl Into<FontSelection>) -> Self {
self.hint_text_font = Some(hint_text_font.into());
self
}
/// If true, hide the letters from view and prevent copying from the field.
#[inline]
pub fn password(mut self, password: bool) -> Self {
@@ -290,10 +297,10 @@ impl<'t> TextEdit<'t> {
self
}
/// Default is `true`. If set to `false` there will be no frame showing that this is editable text!
/// Customize the [`Frame`] around the text edit.
#[inline]
pub fn frame(mut self, frame: bool) -> Self {
self.frame = frame;
pub fn frame(mut self, frame: Frame) -> Self {
self.frame = Some(frame);
self
}
@@ -423,63 +430,18 @@ impl TextEdit<'_> {
/// # });
/// ```
pub fn show(self, ui: &mut Ui) -> TextEditOutput {
let is_mutable = self.text.is_mutable();
let frame = self.frame;
let where_to_put_background = ui.painter().add(Shape::Noop);
let background_color = self
.background_color
.unwrap_or_else(|| ui.visuals().text_edit_bg_color());
let output = self.show_content(ui);
if frame {
let visuals = ui.style().interact(&output.response);
let frame_rect = output.response.rect.expand(visuals.expansion);
let shape = if is_mutable {
if output.response.has_focus() {
epaint::RectShape::new(
frame_rect,
visuals.corner_radius,
background_color,
ui.visuals().selection.stroke,
StrokeKind::Inside,
)
} else {
epaint::RectShape::new(
frame_rect,
visuals.corner_radius,
background_color,
visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop".
StrokeKind::Inside,
)
}
} else {
let visuals = &ui.style().visuals.widgets.inactive;
epaint::RectShape::stroke(
frame_rect,
visuals.corner_radius,
visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop".
StrokeKind::Inside,
)
};
ui.painter().set(where_to_put_background, shape);
}
output
}
fn show_content(self, ui: &mut Ui) -> TextEditOutput {
let TextEdit {
text,
hint_text,
hint_text_font,
prefix,
suffix,
mut hint_text,
id,
id_salt,
font_selection,
text_color,
layouter,
password,
frame: _,
frame,
margin,
multiline,
interactive,
@@ -492,7 +454,7 @@ impl TextEdit<'_> {
clip_text,
char_limit,
return_key,
background_color: _,
background_color,
} = self;
let text_color = text_color
@@ -501,18 +463,16 @@ impl TextEdit<'_> {
.unwrap_or_else(|| ui.visuals().widgets.inactive.text_color());
let prev_text = text.as_str().to_owned();
let hint_text_str = hint_text.text().to_owned();
let hint_text_str = hint_text.text().unwrap_or_default().to_string();
let font_id = font_selection.resolve(ui.style());
let row_height = ui.fonts_mut(|f| f.row_height(&font_id));
const MIN_WIDTH: f32 = 24.0; // Never make a [`TextEdit`] more narrow than this.
let available_width = (ui.available_width() - margin.sum().x).at_least(MIN_WIDTH);
let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width);
let wrap_width = if ui.layout().horizontal_justify() {
available_width
} else {
desired_width.min(available_width)
};
let available_width = ui.available_width().at_least(MIN_WIDTH);
let desired_width = desired_width
.unwrap_or_else(|| ui.spacing().text_edit_width)
.at_least(min_size.x);
let allocate_width = desired_width.at_most(available_width);
let font_id_clone = font_id.clone();
let mut default_layouter = move |ui: &Ui, text: &dyn TextBuffer, wrap_width: f32| {
@@ -527,27 +487,18 @@ impl TextEdit<'_> {
let layouter = layouter.unwrap_or(&mut default_layouter);
let mut galley = layouter(ui, text, wrap_width);
let desired_inner_width = if clip_text {
wrap_width // visual clipping with scroll in singleline input.
} else {
galley.size().x.max(wrap_width)
};
let desired_height = (desired_height_rows.at_least(1) as f32) * row_height;
let desired_inner_size = vec2(desired_inner_width, galley.size().y.max(desired_height));
let desired_outer_size = (desired_inner_size + margin.sum()).at_least(min_size);
let (auto_id, outer_rect) = ui.allocate_space(desired_outer_size);
let rect = outer_rect - margin; // inner rect (excluding frame/margin).
let min_inner_height = (desired_height_rows.at_least(1) as f32) * row_height;
let id = id.unwrap_or_else(|| {
if let Some(id_salt) = id_salt {
ui.make_persistent_id(id_salt)
} else {
auto_id // Since we are only storing the cursor a persistent Id is not super important
// 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
}
});
let mut state = TextEditState::load(ui.ctx(), id).unwrap_or_default();
// On touch screens (e.g. mobile in `eframe` web), should
// dragging select text, or scroll the enclosing [`ScrollArea`] (if any)?
@@ -565,12 +516,215 @@ impl TextEdit<'_> {
} else {
Sense::hover()
};
let mut response = ui.interact(outer_rect, id, sense);
response.intrinsic_size = Some(Vec2::new(desired_width, desired_outer_size.y));
// Don't sent `OutputEvent::Clicked` when a user presses the space bar
let mut state = TextEditState::load(ui.ctx(), id).unwrap_or_default();
let mut cursor_range = None;
let mut prev_cursor_range = None;
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| {
if interactive && ui.memory(|mem| mem.has_focus(id)) {
ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter));
let default_cursor_range = if cursor_at_end {
CCursorRange::one(galley.end())
} else {
CCursorRange::default()
};
prev_cursor_range = state.cursor.range(galley);
let (changed, new_cursor_range) = events(
ui,
&mut state,
text,
galley,
layouter,
id,
wrap_width,
multiline,
password,
default_cursor_range,
char_limit,
event_filter,
return_key,
);
if changed {
text_changed = true;
}
cursor_range = Some(new_cursor_range);
}
};
// 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 mut get_galley = None;
let inner_rect_id = Id::new("text_edit_rect");
let atom_response = {
let any_shrink = hint_text.any_shrink();
// Ideally we could just do `let mut atoms = prefix` here, but that won't compile
// but due to servo/rust-smallvec#146 (also see the comment below).
let mut atoms: Atoms<'_> = Atoms::new(());
// TODO(servo/rust-smallvec#146): Use extend_right instead of the loop once we have
// smallvec 2.0. Using `extend_right` here won't compile, due to lifetime issues.
for atom in prefix {
atoms.push_right(atom);
}
if text.as_str().is_empty() && !hint_text.is_empty() {
// Add hint_text (if any):
let mut shrunk = any_shrink;
let mut first = true;
// 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.
hint_text.map_texts(|t| t.color(ui.style().visuals.weak_text_color()));
for mut atom in hint_text {
if !shrunk && matches!(atom.kind, AtomKind::Text(_)) {
// elide the hint_text if needed
atom = atom.atom_shrink(true);
shrunk = true;
}
if first {
// The first atom in the hint text gets inner_rect_id, so we can know
// where to paint the cursor
atom = atom.atom_id(inner_rect_id);
first = false;
}
// The hint text should be shown left top instead of centered (important for
// multi line text edits)
atoms.push_right(atom.atom_align(Align2::LEFT_TOP));
}
// 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
let available_width = allocate_width - margin.sum().x;
let galley = layouter(ui, text, available_width);
// We can't update the galley immediately here, since it would show both hint text
// and the newly typed letter. So we pass a clone instead, and accept having a frame
// delay on the very first keystroke.
let mut galley_clone = Arc::clone(&galley);
handle_events(ui, &mut galley_clone, layouter, available_width, text);
get_galley = Some(galley);
} else {
// We need a closure here, so we can calculate the galley based on the available
// width (after adding suffix and prefix), for correct wrapping in multi line text
// edits
atoms.push_right(
AtomKind::closure(|ui, args| {
let mut galley = layouter(ui, text, args.available_size.x);
// Handling events here allows us to update the galley immediately on
// keystrokes, avoiding frame delays, and ensuring the scroll_to within
// ScrollAreas works correctly.
handle_events(ui, &mut galley, layouter, args.available_size.x, text);
let intrinsic_size = galley.intrinsic_size();
let mut size = galley.size();
size.y = size.y.at_least(min_inner_height);
if clip_text {
size.x = size.x.at_most(args.available_size.x);
}
// We paint the galley later, so we can do clipping and offsetting
get_galley = Some(galley);
IntoSizedResult {
intrinsic_size,
sized: SizedAtomKind::Empty { size: Some(size) },
}
})
.atom_id(inner_rect_id)
.atom_shrink(clip_text),
);
}
// Ensure the suffix is always right-aligned
if !suffix.is_empty() {
atoms.push_right(Atom::grow());
}
// TODO(servo/rust-smallvec#146): Use extend_right instead of the loop once we have
// smallvec 2.0. Using `extend_right` here won't compile, due to lifetime issues.
for atom in suffix {
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;
// This wrap mode only affects the hint_text
let wrap_mode = if multiline {
TextWrapMode::Wrap
} else {
TextWrapMode::Truncate
};
let mut allocated = AtomLayout::new(atoms)
.id(id)
.min_size(Vec2::new(allocate_width, min_height))
.max_width(allocate_width)
.sense(sense)
.frame(frame)
.align2(Align2::LEFT_TOP)
.wrap_mode(wrap_mode)
.allocate(ui);
allocated.frame = if !custom_frame {
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(stroke.width as i8))
.stroke(stroke)
} else {
allocated.frame
};
allocated.paint(ui)
};
let inner_rect = atom_response.rect(inner_rect_id).unwrap_or(Rect::ZERO);
let mut response = atom_response.response;
// Our atom closure was now called, so the galley should always be available here
let mut galley = get_galley.expect("Galley should be available here");
// Don't send `OutputEvent::Clicked` when a user presses the space bar
response.flags -= response::Flags::FAKE_PRIMARY_CLICKED;
let text_clip_rect = rect;
let text_clip_rect = inner_rect;
let painter = ui.painter_at(text_clip_rect.expand(1.0)); // expand to avoid clipping cursor
if interactive && let Some(pointer_pos) = response.interact_pointer_pos() {
@@ -581,19 +735,19 @@ impl TextEdit<'_> {
// TODO(emilk): drag selected text to either move or clone (ctrl on windows, alt on mac)
let cursor_at_pointer =
galley.cursor_from_pos(pointer_pos - rect.min + state.text_offset);
galley.cursor_from_pos(pointer_pos - inner_rect.min + state.text_offset);
if ui.visuals().text_cursor.preview
&& response.hovered()
&& ui.input(|i| i.pointer.is_moving())
{
// text cursor preview:
let cursor_rect = TSTransform::from_translation(rect.min.to_vec2())
let cursor_rect = TSTransform::from_translation(inner_rect.min.to_vec2())
* cursor_rect(&galley, &cursor_at_pointer, row_height);
text_selection::visuals::paint_cursor_end(&painter, ui.visuals(), cursor_rect);
}
let is_being_dragged = ui.ctx().is_being_dragged(response.id);
let is_being_dragged = ui.is_being_dragged(response.id);
let did_interact = state.cursor.pointer_interaction(
ui,
&response,
@@ -613,44 +767,15 @@ impl TextEdit<'_> {
ui.set_cursor_icon(CursorIcon::Text);
}
let mut cursor_range = None;
let prev_cursor_range = state.cursor.range(&galley);
if interactive && ui.memory(|mem| mem.has_focus(id)) {
ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter));
let default_cursor_range = if cursor_at_end {
CCursorRange::one(galley.end())
} else {
CCursorRange::default()
};
let (changed, new_cursor_range) = events(
ui,
&mut state,
text,
&mut galley,
layouter,
id,
wrap_width,
multiline,
password,
default_cursor_range,
char_limit,
event_filter,
return_key,
);
if changed {
response.mark_changed();
}
cursor_range = Some(new_cursor_range);
if text_changed {
response.mark_changed();
}
let mut galley_pos = align
.align_size_within_rect(galley.size(), rect)
.intersect(rect) // limit pos to the response rect area
.align_size_within_rect(galley.size(), inner_rect)
.intersect(inner_rect) // limit pos to the response rect area
.min;
let align_offset = rect.left_top() - galley_pos;
let align_offset = inner_rect.left_top() - galley_pos;
// Visual clipping for singleline text editor with text larger than width
if clip_text && align_offset.x == 0.0 {
@@ -660,18 +785,18 @@ impl TextEdit<'_> {
};
let mut offset_x = state.text_offset.x;
let visible_range = offset_x..=offset_x + desired_inner_size.x;
let visible_range = offset_x..=offset_x + inner_rect.width();
if !visible_range.contains(&cursor_pos) {
if cursor_pos < *visible_range.start() {
offset_x = cursor_pos;
} else {
offset_x = cursor_pos - desired_inner_size.x;
offset_x = cursor_pos - inner_rect.width();
}
}
offset_x = offset_x
.at_most(galley.size().x - desired_inner_size.x)
.at_most(galley.size().x - inner_rect.width())
.at_least(0.0);
state.text_offset = vec2(offset_x, align_offset.y);
@@ -688,32 +813,7 @@ impl TextEdit<'_> {
false
};
if ui.is_rect_visible(rect) {
if text.as_str().is_empty() && !hint_text.is_empty() {
let hint_text_color = ui.visuals().weak_text_color();
let hint_text_font_id = hint_text_font.unwrap_or_else(|| font_id.into());
let galley = if multiline {
hint_text.into_galley(
ui,
Some(TextWrapMode::Wrap),
desired_inner_size.x,
hint_text_font_id,
)
} else {
hint_text.into_galley(
ui,
Some(TextWrapMode::Extend),
f32::INFINITY,
hint_text_font_id,
)
};
let galley_pos = align
.align_size_within_rect(galley.size(), rect)
.intersect(rect)
.min;
painter.galley(galley_pos, galley, hint_text_color);
}
if ui.is_rect_visible(inner_rect) {
let has_focus = ui.memory(|mem| mem.has_focus(id));
if has_focus && let Some(cursor_range) = state.cursor.range(&galley) {
@@ -721,44 +821,6 @@ impl TextEdit<'_> {
paint_text_selection(&mut galley, ui.visuals(), &cursor_range, None);
}
// Allocate additional space if edits were made this frame that changed the size. This is important so that,
// if there's a ScrollArea, it can properly scroll to the cursor.
// Condition `!clip_text` is important to avoid breaking layout for `TextEdit::singleline` (PR #5640)
if !clip_text
&& let extra_size = galley.size() - rect.size()
&& (extra_size.x > 0.0 || extra_size.y > 0.0)
{
match ui.layout().main_dir() {
crate::Direction::LeftToRight | crate::Direction::TopDown => {
ui.allocate_rect(
Rect::from_min_size(outer_rect.max, extra_size),
Sense::hover(),
);
}
crate::Direction::RightToLeft => {
ui.allocate_rect(
Rect::from_min_size(
emath::pos2(outer_rect.min.x - extra_size.x, outer_rect.max.y),
extra_size,
),
Sense::hover(),
);
}
crate::Direction::BottomUp => {
ui.allocate_rect(
Rect::from_min_size(
emath::pos2(outer_rect.min.x, outer_rect.max.y - extra_size.y),
extra_size,
),
Sense::hover(),
);
}
}
} else {
// Avoid an ID shift during this pass if the textedit grow
ui.skip_ahead_auto_ids(1);
}
painter.galley(galley_pos, Arc::clone(&galley), text_color);
if has_focus && let Some(cursor_range) = state.cursor.range(&galley) {
@@ -767,7 +829,7 @@ impl TextEdit<'_> {
if response.changed() || selection_changed {
// Scroll to keep primary cursor in view:
ui.scroll_to_rect(primary_cursor_rect + margin, None);
ui.scroll_to_rect(primary_cursor_rect, None);
}
if text.is_mutable() && interactive {
@@ -796,9 +858,9 @@ impl TextEdit<'_> {
.layer_transform_to_global(ui.layer_id())
.unwrap_or_default();
ui.ctx().output_mut(|o| {
ui.output_mut(|o| {
o.ime = Some(crate::output::IMEOutput {
rect: to_global * rect,
rect: to_global * inner_rect,
cursor_rect: to_global * primary_cursor_rect,
});
});
@@ -846,24 +908,22 @@ impl TextEdit<'_> {
});
}
{
let role = if password {
accesskit::Role::PasswordInput
} else if multiline {
accesskit::Role::MultilineTextInput
} else {
accesskit::Role::TextInput
};
let role = if password {
accesskit::Role::PasswordInput
} else if multiline {
accesskit::Role::MultilineTextInput
} else {
accesskit::Role::TextInput
};
crate::text_selection::accesskit_text::update_accesskit_for_text_widget(
ui.ctx(),
id,
cursor_range,
role,
TSTransform::from_translation(galley_pos.to_vec2()),
&galley,
);
}
crate::text_selection::accesskit_text::update_accesskit_for_text_widget(
ui.ctx(),
id,
cursor_range,
role,
TSTransform::from_translation(galley_pos.to_vec2()),
&galley,
);
TextEditOutput {
response,
@@ -911,7 +971,7 @@ fn events(
event_filter: EventFilter,
return_key: Option<KeyboardShortcut>,
) -> (bool, CCursorRange) {
let os = ui.ctx().os();
let os = ui.os();
let mut cursor_range = state.cursor.range(galley).unwrap_or(default_cursor_range);

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c8b23a5286e5d2dbd8d3eddac6583d981152bd791f74edfa5c712a610f795256
size 96759
oid sha256:73592be3cb5e2bbc1de870050b913b307e31c05df339b2fd78e9ce38c05f4cd2
size 96758

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a53262cf5d8507d8eeae8c968767cef462b727879245085673982b850a6da670
oid sha256:dfccdafb7e96db488bb5bb8c0a7d25f70e63d900d6b1c2280d218aac0e70e4c4
size 26977

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a7601584308bf60820506f842569a3c1daf3c15fa6e715f6b9386b5112dcc92f
size 76076
oid sha256:b849adf0ff9a06e1f7bfa22ef32b13224c7e62429cf675286110729156434d12
size 76531

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3fc2793506ec483c7f124b6206fb18ffb73bec29746f2d9bb5145042ddc45016
size 114410
oid sha256:8c630df841e98043132b920338793745549116890c1bc52d8d10b0def896a1e6
size 114409

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:718203d31d8b027a7718a66c4712cf1e17b9aea2e870d755bd2c0c346529d4f4
oid sha256:f5e9d645e1decf7624bc8f031b5e28213e64fc5f568dd3eeb1768a57fcb988c3
size 21814

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6af5adc42544171c6d85e190c853aca06784c131a373a693a6f7069d4cf1a404
size 13698
oid sha256:99fa5a5cb10c7d277eafb258af6019eda24a3c96075a50db321f52a521dede92
size 13700

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2e8e03c2a42e195e6489659053aecb78755d3c218558cb2e9339fa7b6db59405
size 35875
oid sha256:1cc61413bcce62cc8e0a55460a974bb56ac40936cd2e5512c4a0e0c521eaaae4
size 35874

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ad22ea6b6e69fd71416fdae76cbd142d279f8f562e74b77e63b3989be187c57c
size 484631
oid sha256:9c81d5a5915dac60297293b90cd3fc0d188a9335e99b318996e3e2934de7bee1
size 483497

View File

@@ -1,6 +1,9 @@
use egui::accesskit::Role;
use egui::epaint::Shape;
use egui::{Align, Color32, Image, Label, Layout, RichText, Sense, TextWrapMode, include_image};
use egui::style::ScrollAnimation;
use egui::{
Align, Color32, Image, Label, Layout, RichText, ScrollArea, Sense, TextWrapMode, include_image,
};
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable as _;
@@ -63,6 +66,67 @@ fn text_edit_rtl() {
}
}
#[test]
fn text_edit_delay() {
let mut text = String::new();
let mut harness = Harness::builder().with_size((200.0, 50.0)).build_ui(|ui| {
ui.style_mut().scroll_animation = ScrollAnimation::none();
ui.add(egui::TextEdit::singleline(&mut text).hint_text("Write something"));
});
harness.get_by_role(Role::TextInput).focus();
harness.step();
harness.snapshot("text_edit_delay_0_empty");
harness.get_by_role(Role::TextInput).type_text("h");
// When the text is empty, and we show the hint text, there is a frame delay.
harness.step();
harness.snapshot("text_edit_delay_1_h_invisible");
// Now it should be visible
harness.step();
harness.snapshot("text_edit_delay_2_h_visible");
harness.get_by_role(Role::TextInput).type_text("i");
// The "i" should immediately be visible without a delay
harness.step();
harness.snapshot("text_edit_delay_3_i_visible");
// The next frame should exactly match the previous one
harness.step();
harness.snapshot("text_edit_delay_4_i_visible");
}
#[test]
fn text_edit_scroll() {
let mut text = "1\n2\n3\n4\n".to_owned();
let mut harness = Harness::builder().build_ui(|ui| {
ScrollArea::vertical().max_height(40.0).show(ui, |ui| {
ui.add(
egui::TextEdit::multiline(&mut text)
.desired_rows(2)
.hint_text("Write something"),
);
});
});
harness.fit_contents();
harness.get_by_role(Role::MultilineTextInput).focus();
harness.step();
harness.snapshot("text_edit_scroll_0_focus");
harness
.get_by_role(Role::MultilineTextInput)
.type_text("5\n");
// When the text is empty, and we show the hint text, there is a frame delay.
harness.run();
harness.snapshot("text_edit_scroll_1_5");
}
#[test]
fn combobox_should_have_value() {
let harness = Harness::new_ui(|ui| {

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:30de3e9f9645206e33fa1edd841b48228e154d0ceae962c64c060a66eecd73ba
size 220452
oid sha256:29363b37f1260f9f39edf9ba873f4c33c0d8a8b6670f6fc178459019539ae7e3
size 220588

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a9f36b8623d2d9c35e337e973f547166f62a5daae757c462b1482babdd42c941
size 383051
oid sha256:94186c0b9331fd0d13284126f4f5e92e66014105fb6533422516d4fbe765e4c7
size 372041

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1a2734644b2fbb6f42ddab6c65a1f5d073f1f002900bbd814c1edb6184e0a9c0
size 362521
oid sha256:8ff058ef716689c309ae9806aaf08fb64eca545ef8f92ce89e1f8e9b7b7733bc
size 330200

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cf00e99dbfdf7497688955feb8c417fab0a366588d92182eccee775abade5179
size 361876

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cf62a248bcec1054cbd97251e6fc429972ef2318c24b9a56698d7c80115aa57e
size 2262

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ef70c95f7e171984f992e1b9366b4a0fe11a4871746cb8cfaa8ee263e59de702
size 2272

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ab120260212d0f41d2956ea2d679cfed648cb188badcca7fa82e0dec9c87ec1a
size 714

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c2ea9d6bef26f1dbd7dc272b4331012ad6fd1a43fded150d33ba4762d404dbde
size 775

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c2ea9d6bef26f1dbd7dc272b4331012ad6fd1a43fded150d33ba4762d404dbde
size 775

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ce357224c2e1cf32f96b3d075dc070c4d14e9aaca1b8165d0ba98603dff19c1b
size 2324
oid sha256:ed3665dfb232b8f0b1483802bfafb4605e8361d7eb977de5a58862e52ab724fa
size 2296

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d91c715ac66be329cac42ff7c7726348b0ac79d897c414bbde26bb0115781577
size 2289
oid sha256:5cfdd6255aba92f0253571bde4f22afc8e0fb5fe3cb882946459d623c0a5d89c
size 2982

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:81620caad6d420f3bd0f224e5b07a02960a42436208a98d3aa012e5db61a743a
size 1510

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f915eafb6490ff456c5b0a7c74c38ef143262bdf74a0c6561b9cf6ee66a679ea
size 1501

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:71daf8a33d277075012bf1130d7820574fe0286080154810d8d398c005a65127
size 9037
oid sha256:60ad2d88535977244ac0fa153700489b454a582af2829dc2f41a531943a21d7a
size 9079

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8d259d113aa23089992b04f19e71c743272dda3fc9baa9612565158f15ace57e
size 8159
oid sha256:7e1fb3fb0a00a447906aa205c27aa496dcb3d79e98aadf6092811a0514efb5a0
size 8127

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a1e56f1b6970c14830d8869f4d8cacfed821ec2b3aab7033b1bfd213a864da79
size 10959
oid sha256:077e7de9fdaaa222ee75f6ad620967fb1e29da37f60407d584be7141e9d0badd
size 10143

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:493649ea09351835147aa6cc800858939dd44beafe37adc488b63b291d58e3b3
size 10302
oid sha256:b022e27d7275764df45039abd26f80d69af40fb18bec98cca85565850df859ae
size 8838

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:337dcbf0b3a344c6cadaf9500376a627739e19e9c47b5da23786c98c612ef4dc
size 10028

View File

@@ -122,6 +122,18 @@ fn widget_tests() {
},
&mut results,
);
test_widget(
"text_edit_prefix_suffix",
|ui| {
ui.spacing_mut().text_edit_width = 45.0;
TextEdit::singleline(&mut "Hello World".to_owned())
.prefix("🔎")
.suffix("!")
.clip_text(true)
.ui(ui)
},
&mut results,
);
test_widget(
"slider",