mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 04:40:03 -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:
@@ -203,7 +203,7 @@ impl<'a> AtomLayout<'a> {
|
|||||||
// If the TextWrapMode is not Extend, ensure there is some item marked as `shrink`.
|
// 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 none is found, mark the first text item as `shrink`.
|
||||||
if wrap_mode != TextWrapMode::Extend {
|
if wrap_mode != TextWrapMode::Extend {
|
||||||
let any_shrink = atoms.iter().any(|a| a.shrink);
|
let any_shrink = atoms.any_shrink();
|
||||||
if !any_shrink {
|
if !any_shrink {
|
||||||
let first_text = atoms
|
let first_text = atoms
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ impl<'a> Atoms<'a> {
|
|||||||
string
|
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>> {
|
pub fn iter_kinds(&self) -> impl Iterator<Item = &AtomKind<'a>> {
|
||||||
self.0.iter().map(|atom| &atom.kind)
|
self.0.iter().map(|atom| &atom.kind)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use emath::{Rect, TSTransform};
|
use emath::{Rect, TSTransform};
|
||||||
use epaint::{
|
use epaint::text::{Galley, LayoutJob, TextWrapMode, cursor::CCursor};
|
||||||
StrokeKind,
|
|
||||||
text::{Galley, LayoutJob, cursor::CCursor},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Align, Align2, Color32, Context, CursorIcon, Event, EventFilter, FontSelection, Id, ImeEvent,
|
Align, Align2, Atom, AtomExt as _, AtomKind, AtomLayout, Atoms, Color32, Context, CursorIcon,
|
||||||
Key, KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, Shape, TextBuffer,
|
Event, EventFilter, FontSelection, Frame, Id, ImeEvent, IntoAtoms, IntoSizedResult, Key,
|
||||||
TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetWithState, epaint,
|
KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, SizedAtomKind, TextBuffer,
|
||||||
|
TextStyle, Ui, Vec2, Widget, WidgetInfo, WidgetWithState, epaint,
|
||||||
os::OperatingSystem,
|
os::OperatingSystem,
|
||||||
output::OutputEvent,
|
output::OutputEvent,
|
||||||
response, text_selection,
|
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);`"]
|
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
|
||||||
pub struct TextEdit<'t> {
|
pub struct TextEdit<'t> {
|
||||||
text: &'t mut dyn TextBuffer,
|
text: &'t mut dyn TextBuffer,
|
||||||
hint_text: WidgetText,
|
prefix: Atoms<'static>,
|
||||||
hint_text_font: Option<FontSelection>,
|
suffix: Atoms<'static>,
|
||||||
|
hint_text: Atoms<'static>,
|
||||||
id: Option<Id>,
|
id: Option<Id>,
|
||||||
id_salt: Option<Id>,
|
id_salt: Option<Id>,
|
||||||
font_selection: FontSelection,
|
font_selection: FontSelection,
|
||||||
text_color: Option<Color32>,
|
text_color: Option<Color32>,
|
||||||
layouter: Option<LayouterFn<'t>>,
|
layouter: Option<LayouterFn<'t>>,
|
||||||
password: bool,
|
password: bool,
|
||||||
frame: bool,
|
frame: Option<Frame>,
|
||||||
margin: Margin,
|
margin: Margin,
|
||||||
multiline: bool,
|
multiline: bool,
|
||||||
interactive: bool,
|
interactive: bool,
|
||||||
@@ -120,15 +119,16 @@ impl<'t> TextEdit<'t> {
|
|||||||
pub fn multiline(text: &'t mut dyn TextBuffer) -> Self {
|
pub fn multiline(text: &'t mut dyn TextBuffer) -> Self {
|
||||||
Self {
|
Self {
|
||||||
text,
|
text,
|
||||||
|
prefix: Default::default(),
|
||||||
|
suffix: Default::default(),
|
||||||
hint_text: Default::default(),
|
hint_text: Default::default(),
|
||||||
hint_text_font: None,
|
|
||||||
id: None,
|
id: None,
|
||||||
id_salt: None,
|
id_salt: None,
|
||||||
font_selection: Default::default(),
|
font_selection: Default::default(),
|
||||||
text_color: None,
|
text_color: None,
|
||||||
layouter: None,
|
layouter: None,
|
||||||
password: false,
|
password: false,
|
||||||
frame: true,
|
frame: None,
|
||||||
margin: Margin::symmetric(4, 2),
|
margin: Margin::symmetric(4, 2),
|
||||||
multiline: true,
|
multiline: true,
|
||||||
interactive: true,
|
interactive: true,
|
||||||
@@ -202,8 +202,22 @@ impl<'t> TextEdit<'t> {
|
|||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn hint_text(mut self, hint_text: impl Into<WidgetText>) -> Self {
|
pub fn hint_text(mut self, hint_text: impl IntoAtoms<'static>) -> Self {
|
||||||
self.hint_text = hint_text.into();
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,13 +229,6 @@ impl<'t> TextEdit<'t> {
|
|||||||
self
|
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.
|
/// If true, hide the letters from view and prevent copying from the field.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn password(mut self, password: bool) -> Self {
|
pub fn password(mut self, password: bool) -> Self {
|
||||||
@@ -290,10 +297,10 @@ impl<'t> TextEdit<'t> {
|
|||||||
self
|
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]
|
#[inline]
|
||||||
pub fn frame(mut self, frame: bool) -> Self {
|
pub fn frame(mut self, frame: Frame) -> Self {
|
||||||
self.frame = frame;
|
self.frame = Some(frame);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,63 +430,18 @@ impl TextEdit<'_> {
|
|||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
pub fn show(self, ui: &mut Ui) -> TextEditOutput {
|
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 {
|
let TextEdit {
|
||||||
text,
|
text,
|
||||||
hint_text,
|
prefix,
|
||||||
hint_text_font,
|
suffix,
|
||||||
|
mut hint_text,
|
||||||
id,
|
id,
|
||||||
id_salt,
|
id_salt,
|
||||||
font_selection,
|
font_selection,
|
||||||
text_color,
|
text_color,
|
||||||
layouter,
|
layouter,
|
||||||
password,
|
password,
|
||||||
frame: _,
|
frame,
|
||||||
margin,
|
margin,
|
||||||
multiline,
|
multiline,
|
||||||
interactive,
|
interactive,
|
||||||
@@ -492,7 +454,7 @@ impl TextEdit<'_> {
|
|||||||
clip_text,
|
clip_text,
|
||||||
char_limit,
|
char_limit,
|
||||||
return_key,
|
return_key,
|
||||||
background_color: _,
|
background_color,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let text_color = text_color
|
let text_color = text_color
|
||||||
@@ -501,18 +463,16 @@ impl TextEdit<'_> {
|
|||||||
.unwrap_or_else(|| ui.visuals().widgets.inactive.text_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().to_owned();
|
let hint_text_str = hint_text.text().unwrap_or_default().to_string();
|
||||||
|
|
||||||
let font_id = font_selection.resolve(ui.style());
|
let font_id = 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));
|
||||||
const MIN_WIDTH: f32 = 24.0; // Never make a [`TextEdit`] more narrow than this.
|
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 available_width = ui.available_width().at_least(MIN_WIDTH);
|
||||||
let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width);
|
let desired_width = desired_width
|
||||||
let wrap_width = if ui.layout().horizontal_justify() {
|
.unwrap_or_else(|| ui.spacing().text_edit_width)
|
||||||
available_width
|
.at_least(min_size.x);
|
||||||
} else {
|
let allocate_width = desired_width.at_most(available_width);
|
||||||
desired_width.min(available_width)
|
|
||||||
};
|
|
||||||
|
|
||||||
let font_id_clone = font_id.clone();
|
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| {
|
||||||
@@ -527,27 +487,18 @@ impl TextEdit<'_> {
|
|||||||
|
|
||||||
let layouter = layouter.unwrap_or(&mut default_layouter);
|
let layouter = layouter.unwrap_or(&mut default_layouter);
|
||||||
|
|
||||||
let mut galley = layouter(ui, text, wrap_width);
|
let min_inner_height = (desired_height_rows.at_least(1) as f32) * row_height;
|
||||||
|
|
||||||
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 id = id.unwrap_or_else(|| {
|
let id = id.unwrap_or_else(|| {
|
||||||
if let Some(id_salt) = id_salt {
|
if let Some(id_salt) = id_salt {
|
||||||
ui.make_persistent_id(id_salt)
|
ui.make_persistent_id(id_salt)
|
||||||
} else {
|
} 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
|
// 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)?
|
||||||
@@ -565,12 +516,215 @@ impl TextEdit<'_> {
|
|||||||
} else {
|
} else {
|
||||||
Sense::hover()
|
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;
|
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
|
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() {
|
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)
|
// TODO(emilk): drag selected text to either move or clone (ctrl on windows, alt on mac)
|
||||||
|
|
||||||
let cursor_at_pointer =
|
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
|
if ui.visuals().text_cursor.preview
|
||||||
&& response.hovered()
|
&& response.hovered()
|
||||||
&& ui.input(|i| i.pointer.is_moving())
|
&& ui.input(|i| i.pointer.is_moving())
|
||||||
{
|
{
|
||||||
// text cursor preview:
|
// 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);
|
* cursor_rect(&galley, &cursor_at_pointer, row_height);
|
||||||
text_selection::visuals::paint_cursor_end(&painter, ui.visuals(), cursor_rect);
|
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(
|
let did_interact = state.cursor.pointer_interaction(
|
||||||
ui,
|
ui,
|
||||||
&response,
|
&response,
|
||||||
@@ -613,44 +767,15 @@ impl TextEdit<'_> {
|
|||||||
ui.set_cursor_icon(CursorIcon::Text);
|
ui.set_cursor_icon(CursorIcon::Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut cursor_range = None;
|
if text_changed {
|
||||||
let prev_cursor_range = state.cursor.range(&galley);
|
response.mark_changed();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut galley_pos = align
|
let mut galley_pos = align
|
||||||
.align_size_within_rect(galley.size(), rect)
|
.align_size_within_rect(galley.size(), inner_rect)
|
||||||
.intersect(rect) // limit pos to the response rect area
|
.intersect(inner_rect) // limit pos to the response rect area
|
||||||
.min;
|
.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
|
// Visual clipping for singleline text editor with text larger than width
|
||||||
if clip_text && align_offset.x == 0.0 {
|
if clip_text && align_offset.x == 0.0 {
|
||||||
@@ -660,18 +785,18 @@ impl TextEdit<'_> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut offset_x = state.text_offset.x;
|
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 !visible_range.contains(&cursor_pos) {
|
||||||
if cursor_pos < *visible_range.start() {
|
if cursor_pos < *visible_range.start() {
|
||||||
offset_x = cursor_pos;
|
offset_x = cursor_pos;
|
||||||
} else {
|
} else {
|
||||||
offset_x = cursor_pos - desired_inner_size.x;
|
offset_x = cursor_pos - inner_rect.width();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
offset_x = offset_x
|
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);
|
.at_least(0.0);
|
||||||
|
|
||||||
state.text_offset = vec2(offset_x, align_offset.y);
|
state.text_offset = vec2(offset_x, align_offset.y);
|
||||||
@@ -688,32 +813,7 @@ impl TextEdit<'_> {
|
|||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
if ui.is_rect_visible(rect) {
|
if ui.is_rect_visible(inner_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);
|
|
||||||
}
|
|
||||||
|
|
||||||
let has_focus = ui.memory(|mem| mem.has_focus(id));
|
let has_focus = ui.memory(|mem| mem.has_focus(id));
|
||||||
|
|
||||||
if has_focus && let Some(cursor_range) = state.cursor.range(&galley) {
|
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);
|
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);
|
painter.galley(galley_pos, Arc::clone(&galley), text_color);
|
||||||
|
|
||||||
if has_focus && let Some(cursor_range) = state.cursor.range(&galley) {
|
if has_focus && let Some(cursor_range) = state.cursor.range(&galley) {
|
||||||
@@ -767,7 +829,7 @@ impl TextEdit<'_> {
|
|||||||
|
|
||||||
if response.changed() || selection_changed {
|
if response.changed() || selection_changed {
|
||||||
// Scroll to keep primary cursor in view:
|
// 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 {
|
if text.is_mutable() && interactive {
|
||||||
@@ -796,9 +858,9 @@ impl TextEdit<'_> {
|
|||||||
.layer_transform_to_global(ui.layer_id())
|
.layer_transform_to_global(ui.layer_id())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
ui.ctx().output_mut(|o| {
|
ui.output_mut(|o| {
|
||||||
o.ime = Some(crate::output::IMEOutput {
|
o.ime = Some(crate::output::IMEOutput {
|
||||||
rect: to_global * rect,
|
rect: to_global * inner_rect,
|
||||||
cursor_rect: to_global * primary_cursor_rect,
|
cursor_rect: to_global * primary_cursor_rect,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -846,24 +908,22 @@ impl TextEdit<'_> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
let role = if password {
|
||||||
let role = if password {
|
accesskit::Role::PasswordInput
|
||||||
accesskit::Role::PasswordInput
|
} else if multiline {
|
||||||
} else if multiline {
|
accesskit::Role::MultilineTextInput
|
||||||
accesskit::Role::MultilineTextInput
|
} else {
|
||||||
} else {
|
accesskit::Role::TextInput
|
||||||
accesskit::Role::TextInput
|
};
|
||||||
};
|
|
||||||
|
|
||||||
crate::text_selection::accesskit_text::update_accesskit_for_text_widget(
|
crate::text_selection::accesskit_text::update_accesskit_for_text_widget(
|
||||||
ui.ctx(),
|
ui.ctx(),
|
||||||
id,
|
id,
|
||||||
cursor_range,
|
cursor_range,
|
||||||
role,
|
role,
|
||||||
TSTransform::from_translation(galley_pos.to_vec2()),
|
TSTransform::from_translation(galley_pos.to_vec2()),
|
||||||
&galley,
|
&galley,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
TextEditOutput {
|
TextEditOutput {
|
||||||
response,
|
response,
|
||||||
@@ -911,7 +971,7 @@ fn events(
|
|||||||
event_filter: EventFilter,
|
event_filter: EventFilter,
|
||||||
return_key: Option<KeyboardShortcut>,
|
return_key: Option<KeyboardShortcut>,
|
||||||
) -> (bool, CCursorRange) {
|
) -> (bool, CCursorRange) {
|
||||||
let os = ui.ctx().os();
|
let os = ui.os();
|
||||||
|
|
||||||
let mut cursor_range = state.cursor.range(galley).unwrap_or(default_cursor_range);
|
let mut cursor_range = state.cursor.range(galley).unwrap_or(default_cursor_range);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c8b23a5286e5d2dbd8d3eddac6583d981152bd791f74edfa5c712a610f795256
|
oid sha256:73592be3cb5e2bbc1de870050b913b307e31c05df339b2fd78e9ce38c05f4cd2
|
||||||
size 96759
|
size 96758
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a53262cf5d8507d8eeae8c968767cef462b727879245085673982b850a6da670
|
oid sha256:dfccdafb7e96db488bb5bb8c0a7d25f70e63d900d6b1c2280d218aac0e70e4c4
|
||||||
size 26977
|
size 26977
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a7601584308bf60820506f842569a3c1daf3c15fa6e715f6b9386b5112dcc92f
|
oid sha256:b849adf0ff9a06e1f7bfa22ef32b13224c7e62429cf675286110729156434d12
|
||||||
size 76076
|
size 76531
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3fc2793506ec483c7f124b6206fb18ffb73bec29746f2d9bb5145042ddc45016
|
oid sha256:8c630df841e98043132b920338793745549116890c1bc52d8d10b0def896a1e6
|
||||||
size 114410
|
size 114409
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:718203d31d8b027a7718a66c4712cf1e17b9aea2e870d755bd2c0c346529d4f4
|
oid sha256:f5e9d645e1decf7624bc8f031b5e28213e64fc5f568dd3eeb1768a57fcb988c3
|
||||||
size 21814
|
size 21814
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6af5adc42544171c6d85e190c853aca06784c131a373a693a6f7069d4cf1a404
|
oid sha256:99fa5a5cb10c7d277eafb258af6019eda24a3c96075a50db321f52a521dede92
|
||||||
size 13698
|
size 13700
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2e8e03c2a42e195e6489659053aecb78755d3c218558cb2e9339fa7b6db59405
|
oid sha256:1cc61413bcce62cc8e0a55460a974bb56ac40936cd2e5512c4a0e0c521eaaae4
|
||||||
size 35875
|
size 35874
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ad22ea6b6e69fd71416fdae76cbd142d279f8f562e74b77e63b3989be187c57c
|
oid sha256:9c81d5a5915dac60297293b90cd3fc0d188a9335e99b318996e3e2934de7bee1
|
||||||
size 484631
|
size 483497
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
use egui::accesskit::Role;
|
use egui::accesskit::Role;
|
||||||
use egui::epaint::Shape;
|
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::Harness;
|
||||||
use egui_kittest::kittest::Queryable as _;
|
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]
|
#[test]
|
||||||
fn combobox_should_have_value() {
|
fn combobox_should_have_value() {
|
||||||
let harness = Harness::new_ui(|ui| {
|
let harness = Harness::new_ui(|ui| {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:30de3e9f9645206e33fa1edd841b48228e154d0ceae962c64c060a66eecd73ba
|
oid sha256:29363b37f1260f9f39edf9ba873f4c33c0d8a8b6670f6fc178459019539ae7e3
|
||||||
size 220452
|
size 220588
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a9f36b8623d2d9c35e337e973f547166f62a5daae757c462b1482babdd42c941
|
oid sha256:94186c0b9331fd0d13284126f4f5e92e66014105fb6533422516d4fbe765e4c7
|
||||||
size 383051
|
size 372041
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1a2734644b2fbb6f42ddab6c65a1f5d073f1f002900bbd814c1edb6184e0a9c0
|
oid sha256:8ff058ef716689c309ae9806aaf08fb64eca545ef8f92ce89e1f8e9b7b7733bc
|
||||||
size 362521
|
size 330200
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:cf00e99dbfdf7497688955feb8c417fab0a366588d92182eccee775abade5179
|
||||||
|
size 361876
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:cf62a248bcec1054cbd97251e6fc429972ef2318c24b9a56698d7c80115aa57e
|
||||||
|
size 2262
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ef70c95f7e171984f992e1b9366b4a0fe11a4871746cb8cfaa8ee263e59de702
|
||||||
|
size 2272
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ab120260212d0f41d2956ea2d679cfed648cb188badcca7fa82e0dec9c87ec1a
|
||||||
|
size 714
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c2ea9d6bef26f1dbd7dc272b4331012ad6fd1a43fded150d33ba4762d404dbde
|
||||||
|
size 775
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c2ea9d6bef26f1dbd7dc272b4331012ad6fd1a43fded150d33ba4762d404dbde
|
||||||
|
size 775
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ce357224c2e1cf32f96b3d075dc070c4d14e9aaca1b8165d0ba98603dff19c1b
|
oid sha256:ed3665dfb232b8f0b1483802bfafb4605e8361d7eb977de5a58862e52ab724fa
|
||||||
size 2324
|
size 2296
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d91c715ac66be329cac42ff7c7726348b0ac79d897c414bbde26bb0115781577
|
oid sha256:5cfdd6255aba92f0253571bde4f22afc8e0fb5fe3cb882946459d623c0a5d89c
|
||||||
size 2289
|
size 2982
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:81620caad6d420f3bd0f224e5b07a02960a42436208a98d3aa012e5db61a743a
|
||||||
|
size 1510
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:f915eafb6490ff456c5b0a7c74c38ef143262bdf74a0c6561b9cf6ee66a679ea
|
||||||
|
size 1501
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:71daf8a33d277075012bf1130d7820574fe0286080154810d8d398c005a65127
|
oid sha256:60ad2d88535977244ac0fa153700489b454a582af2829dc2f41a531943a21d7a
|
||||||
size 9037
|
size 9079
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8d259d113aa23089992b04f19e71c743272dda3fc9baa9612565158f15ace57e
|
oid sha256:7e1fb3fb0a00a447906aa205c27aa496dcb3d79e98aadf6092811a0514efb5a0
|
||||||
size 8159
|
size 8127
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a1e56f1b6970c14830d8869f4d8cacfed821ec2b3aab7033b1bfd213a864da79
|
oid sha256:077e7de9fdaaa222ee75f6ad620967fb1e29da37f60407d584be7141e9d0badd
|
||||||
size 10959
|
size 10143
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:493649ea09351835147aa6cc800858939dd44beafe37adc488b63b291d58e3b3
|
oid sha256:b022e27d7275764df45039abd26f80d69af40fb18bec98cca85565850df859ae
|
||||||
size 10302
|
size 8838
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:337dcbf0b3a344c6cadaf9500376a627739e19e9c47b5da23786c98c612ef4dc
|
||||||
|
size 10028
|
||||||
@@ -122,6 +122,18 @@ fn widget_tests() {
|
|||||||
},
|
},
|
||||||
&mut results,
|
&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(
|
test_widget(
|
||||||
"slider",
|
"slider",
|
||||||
|
|||||||
Reference in New Issue
Block a user