diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/atom_layout.rs index b78f23536..da7f672a1 100644 --- a/crates/egui/src/atomics/atom_layout.rs +++ b/crates/egui/src/atomics/atom_layout.rs @@ -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() diff --git a/crates/egui/src/atomics/atoms.rs b/crates/egui/src/atomics/atoms.rs index fb04ee2dd..5051a7676 100644 --- a/crates/egui/src/atomics/atoms.rs +++ b/crates/egui/src/atomics/atoms.rs @@ -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> { self.0.iter().map(|atom| &atom.kind) } diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index b9fdb1cbe..50154102a 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -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 { text: &'t mut dyn TextBuffer, - hint_text: WidgetText, - hint_text_font: Option, + prefix: Atoms<'static>, + suffix: Atoms<'static>, + hint_text: Atoms<'static>, id: Option, id_salt: Option, font_selection: FontSelection, text_color: Option, layouter: Option>, password: bool, - frame: bool, + frame: Option, 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) -> 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) -> 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, 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, ) -> (bool, CCursorRange) { - let os = ui.ctx().os(); + let os = ui.os(); let mut cursor_range = state.cursor.range(galley).unwrap_or(default_cursor_range); diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png index 8d5f688bf..a97c3ea8b 100644 --- a/crates/egui_demo_app/tests/snapshots/imageviewer.png +++ b/crates/egui_demo_app/tests/snapshots/imageviewer.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8b23a5286e5d2dbd8d3eddac6583d981152bd791f74edfa5c712a610f795256 -size 96759 +oid sha256:73592be3cb5e2bbc1de870050b913b307e31c05df339b2fd78e9ce38c05f4cd2 +size 96758 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png index 2549417be..47ad5bc7a 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a53262cf5d8507d8eeae8c968767cef462b727879245085673982b850a6da670 +oid sha256:dfccdafb7e96db488bb5bb8c0a7d25f70e63d900d6b1c2280d218aac0e70e4c4 size 26977 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png index 46a5ed1f7..8c6077a1c 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7601584308bf60820506f842569a3c1daf3c15fa6e715f6b9386b5112dcc92f -size 76076 +oid sha256:b849adf0ff9a06e1f7bfa22ef32b13224c7e62429cf675286110729156434d12 +size 76531 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png index 375b0f922..f73093e3e 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3fc2793506ec483c7f124b6206fb18ffb73bec29746f2d9bb5145042ddc45016 -size 114410 +oid sha256:8c630df841e98043132b920338793745549116890c1bc52d8d10b0def896a1e6 +size 114409 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png index 843bb93b4..6b1a9946b 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:718203d31d8b027a7718a66c4712cf1e17b9aea2e870d755bd2c0c346529d4f4 +oid sha256:f5e9d645e1decf7624bc8f031b5e28213e64fc5f568dd3eeb1768a57fcb988c3 size 21814 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png index 81204a347..92992cd83 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6af5adc42544171c6d85e190c853aca06784c131a373a693a6f7069d4cf1a404 -size 13698 +oid sha256:99fa5a5cb10c7d277eafb258af6019eda24a3c96075a50db321f52a521dede92 +size 13700 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png index 78446cca9..cdc1a43dd 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e8e03c2a42e195e6489659053aecb78755d3c218558cb2e9339fa7b6db59405 -size 35875 +oid sha256:1cc61413bcce62cc8e0a55460a974bb56ac40936cd2e5512c4a0e0c521eaaae4 +size 35874 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png index a6d103d6d..96ca9949e 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad22ea6b6e69fd71416fdae76cbd142d279f8f562e74b77e63b3989be187c57c -size 484631 +oid sha256:9c81d5a5915dac60297293b90cd3fc0d188a9335e99b318996e3e2934de7bee1 +size 483497 diff --git a/tests/egui_tests/tests/regression_tests.rs b/tests/egui_tests/tests/regression_tests.rs index 421b69d35..d61b76af3 100644 --- a/tests/egui_tests/tests/regression_tests.rs +++ b/tests/egui_tests/tests/regression_tests.rs @@ -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| { diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit.png b/tests/egui_tests/tests/snapshots/layout/text_edit.png index cfbaefd41..d5d853f5e 100644 --- a/tests/egui_tests/tests/snapshots/layout/text_edit.png +++ b/tests/egui_tests/tests/snapshots/layout/text_edit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:30de3e9f9645206e33fa1edd841b48228e154d0ceae962c64c060a66eecd73ba -size 220452 +oid sha256:29363b37f1260f9f39edf9ba873f4c33c0d8a8b6670f6fc178459019539ae7e3 +size 220588 diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit_clip.png b/tests/egui_tests/tests/snapshots/layout/text_edit_clip.png index 8ea5d0b7a..d87f37561 100644 --- a/tests/egui_tests/tests/snapshots/layout/text_edit_clip.png +++ b/tests/egui_tests/tests/snapshots/layout/text_edit_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9f36b8623d2d9c35e337e973f547166f62a5daae757c462b1482babdd42c941 -size 383051 +oid sha256:94186c0b9331fd0d13284126f4f5e92e66014105fb6533422516d4fbe765e4c7 +size 372041 diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit_placeholder_clip.png b/tests/egui_tests/tests/snapshots/layout/text_edit_placeholder_clip.png index 0a84f42db..578e4d9db 100644 --- a/tests/egui_tests/tests/snapshots/layout/text_edit_placeholder_clip.png +++ b/tests/egui_tests/tests/snapshots/layout/text_edit_placeholder_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a2734644b2fbb6f42ddab6c65a1f5d073f1f002900bbd814c1edb6184e0a9c0 -size 362521 +oid sha256:8ff058ef716689c309ae9806aaf08fb64eca545ef8f92ce89e1f8e9b7b7733bc +size 330200 diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit_prefix_suffix.png b/tests/egui_tests/tests/snapshots/layout/text_edit_prefix_suffix.png new file mode 100644 index 000000000..bdcab38f2 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/layout/text_edit_prefix_suffix.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf00e99dbfdf7497688955feb8c417fab0a366588d92182eccee775abade5179 +size 361876 diff --git a/tests/egui_tests/tests/snapshots/text_edit_delay_0_empty.png b/tests/egui_tests/tests/snapshots/text_edit_delay_0_empty.png new file mode 100644 index 000000000..58b0b13f2 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/text_edit_delay_0_empty.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf62a248bcec1054cbd97251e6fc429972ef2318c24b9a56698d7c80115aa57e +size 2262 diff --git a/tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png b/tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png new file mode 100644 index 000000000..c1920bcf1 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef70c95f7e171984f992e1b9366b4a0fe11a4871746cb8cfaa8ee263e59de702 +size 2272 diff --git a/tests/egui_tests/tests/snapshots/text_edit_delay_2_h_visible.png b/tests/egui_tests/tests/snapshots/text_edit_delay_2_h_visible.png new file mode 100644 index 000000000..bc1ffcd08 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/text_edit_delay_2_h_visible.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab120260212d0f41d2956ea2d679cfed648cb188badcca7fa82e0dec9c87ec1a +size 714 diff --git a/tests/egui_tests/tests/snapshots/text_edit_delay_3_i_visible.png b/tests/egui_tests/tests/snapshots/text_edit_delay_3_i_visible.png new file mode 100644 index 000000000..324946e10 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/text_edit_delay_3_i_visible.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2ea9d6bef26f1dbd7dc272b4331012ad6fd1a43fded150d33ba4762d404dbde +size 775 diff --git a/tests/egui_tests/tests/snapshots/text_edit_delay_4_i_visible.png b/tests/egui_tests/tests/snapshots/text_edit_delay_4_i_visible.png new file mode 100644 index 000000000..324946e10 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/text_edit_delay_4_i_visible.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2ea9d6bef26f1dbd7dc272b4331012ad6fd1a43fded150d33ba4762d404dbde +size 775 diff --git a/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png b/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png index 3b87786e8..796a1e3b5 100644 --- a/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png +++ b/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce357224c2e1cf32f96b3d075dc070c4d14e9aaca1b8165d0ba98603dff19c1b -size 2324 +oid sha256:ed3665dfb232b8f0b1483802bfafb4605e8361d7eb977de5a58862e52ab724fa +size 2296 diff --git a/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png b/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png index 5d9aada78..cd6d5a621 100644 --- a/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png +++ b/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d91c715ac66be329cac42ff7c7726348b0ac79d897c414bbde26bb0115781577 -size 2289 +oid sha256:5cfdd6255aba92f0253571bde4f22afc8e0fb5fe3cb882946459d623c0a5d89c +size 2982 diff --git a/tests/egui_tests/tests/snapshots/text_edit_scroll_0_focus.png b/tests/egui_tests/tests/snapshots/text_edit_scroll_0_focus.png new file mode 100644 index 000000000..1d0a5ed46 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/text_edit_scroll_0_focus.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81620caad6d420f3bd0f224e5b07a02960a42436208a98d3aa012e5db61a743a +size 1510 diff --git a/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png b/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png new file mode 100644 index 000000000..bdb8d1b1b --- /dev/null +++ b/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f915eafb6490ff456c5b0a7c74c38ef143262bdf74a0c6561b9cf6ee66a679ea +size 1501 diff --git a/tests/egui_tests/tests/snapshots/visuals/drag_value.png b/tests/egui_tests/tests/snapshots/visuals/drag_value.png index 8be9c5e9f..7cd2d2ce8 100644 --- a/tests/egui_tests/tests/snapshots/visuals/drag_value.png +++ b/tests/egui_tests/tests/snapshots/visuals/drag_value.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71daf8a33d277075012bf1130d7820574fe0286080154810d8d398c005a65127 -size 9037 +oid sha256:60ad2d88535977244ac0fa153700489b454a582af2829dc2f41a531943a21d7a +size 9079 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit.png b/tests/egui_tests/tests/snapshots/visuals/text_edit.png index 4de5e3bd0..4719c8ce9 100644 --- a/tests/egui_tests/tests/snapshots/visuals/text_edit.png +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d259d113aa23089992b04f19e71c743272dda3fc9baa9612565158f15ace57e -size 8159 +oid sha256:7e1fb3fb0a00a447906aa205c27aa496dcb3d79e98aadf6092811a0514efb5a0 +size 8127 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png index 192fa8f74..8a5999742 100644 --- a/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1e56f1b6970c14830d8869f4d8cacfed821ec2b3aab7033b1bfd213a864da79 -size 10959 +oid sha256:077e7de9fdaaa222ee75f6ad620967fb1e29da37f60407d584be7141e9d0badd +size 10143 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png index decd09bf9..19c231b45 100644 --- a/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:493649ea09351835147aa6cc800858939dd44beafe37adc488b63b291d58e3b3 -size 10302 +oid sha256:b022e27d7275764df45039abd26f80d69af40fb18bec98cca85565850df859ae +size 8838 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png new file mode 100644 index 000000000..d27f6f8c4 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:337dcbf0b3a344c6cadaf9500376a627739e19e9c47b5da23786c98c612ef4dc +size 10028 diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs index 5ef98c8a8..5283b21d4 100644 --- a/tests/egui_tests/tests/test_widgets.rs +++ b/tests/egui_tests/tests/test_widgets.rs @@ -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",