mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 22:53:14 -04:00
Fix centered & right aligned TextEdit (#8082)
A couple improvements to centered and right-aligned text edits: - Fix text selection in centered and right aligned text edits (ironically, this broke in #8076) - Fix cursor movement in centered and right aligned text edits (horizontal cursor position will be retained on vertical movement) - Multiline text edit exceeding available width if there are atoms - Added atoms & alignment options to text edit demo - Improve how vertical_align and horizontal_align are applied - Textedit atom is grow now, removing the need for the extra seperate grow atom - This allows us to apply the `align` on the text edit atom instead of the whole AtomLayout - Fixes https://github.com/emilk/egui/pull/8022 - Fixes https://github.com/emilk/egui/issues/7999
This commit is contained in:
@@ -4,8 +4,8 @@ use emath::{Rect, TSTransform};
|
|||||||
use epaint::text::{Galley, LayoutJob, TextWrapMode, cursor::CCursor};
|
use epaint::text::{Galley, LayoutJob, TextWrapMode, cursor::CCursor};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Align, Align2, Atom, AtomExt as _, AtomKind, AtomLayout, Atoms, Color32, Context, CursorIcon,
|
Align, Align2, AtomExt as _, AtomKind, AtomLayout, Atoms, Color32, Context, CursorIcon, Event,
|
||||||
Event, EventFilter, FontSelection, Frame, Id, ImeEvent, IntoAtoms, IntoSizedResult, Key,
|
EventFilter, FontSelection, Frame, Id, ImeEvent, IntoAtoms, IntoSizedResult, Key,
|
||||||
KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, SizedAtomKind, TextBuffer,
|
KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, SizedAtomKind, TextBuffer,
|
||||||
TextStyle, Ui, Vec2, Widget, WidgetInfo, WidgetWithState, epaint,
|
TextStyle, Ui, Vec2, Widget, WidgetInfo, WidgetWithState, epaint,
|
||||||
os::OperatingSystem,
|
os::OperatingSystem,
|
||||||
@@ -480,11 +480,12 @@ impl TextEdit<'_> {
|
|||||||
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| {
|
||||||
let text = mask_if_password(password, text.as_str());
|
let text = mask_if_password(password, text.as_str());
|
||||||
let layout_job = if multiline {
|
let mut layout_job = if multiline {
|
||||||
LayoutJob::simple(text, font_id_clone.clone(), text_color, wrap_width)
|
LayoutJob::simple(text, font_id_clone.clone(), text_color, wrap_width)
|
||||||
} else {
|
} else {
|
||||||
LayoutJob::simple_singleline(text, font_id_clone.clone(), text_color)
|
LayoutJob::simple_singleline(text, font_id_clone.clone(), text_color)
|
||||||
};
|
};
|
||||||
|
layout_job.halign = align.x();
|
||||||
ui.fonts_mut(|f| f.layout_job(layout_job))
|
ui.fonts_mut(|f| f.layout_job(layout_job))
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -591,6 +592,7 @@ impl TextEdit<'_> {
|
|||||||
if !shrunk && matches!(atom.kind, AtomKind::Text(_)) {
|
if !shrunk && matches!(atom.kind, AtomKind::Text(_)) {
|
||||||
// elide the hint_text if needed
|
// elide the hint_text if needed
|
||||||
atom = atom.atom_shrink(true);
|
atom = atom.atom_shrink(true);
|
||||||
|
atom = atom.atom_grow(true);
|
||||||
shrunk = true;
|
shrunk = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -619,6 +621,11 @@ impl TextEdit<'_> {
|
|||||||
|
|
||||||
get_galley = Some(galley);
|
get_galley = Some(galley);
|
||||||
} else {
|
} else {
|
||||||
|
// We need to shrink when clip_text, so that we don't exceed the available size
|
||||||
|
// and thus clip. We also need to shrink in multi line text edits, so text can
|
||||||
|
// wrap appropriately.
|
||||||
|
let should_shrink = clip_text || multiline;
|
||||||
|
|
||||||
// We need a closure here, so we can calculate the galley based on the available
|
// 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
|
// width (after adding suffix and prefix), for correct wrapping in multi line text
|
||||||
// edits
|
// edits
|
||||||
@@ -645,16 +652,13 @@ impl TextEdit<'_> {
|
|||||||
sized: SizedAtomKind::Empty { size: Some(size) },
|
sized: SizedAtomKind::Empty { size: Some(size) },
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.atom_grow(true)
|
||||||
|
.atom_align(self.align)
|
||||||
.atom_id(inner_rect_id)
|
.atom_id(inner_rect_id)
|
||||||
.atom_shrink(clip_text),
|
.atom_shrink(should_shrink),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// 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.
|
// smallvec 2.0. Using `extend_right` here won't compile, due to lifetime issues.
|
||||||
for atom in suffix {
|
for atom in suffix {
|
||||||
@@ -679,7 +683,7 @@ impl TextEdit<'_> {
|
|||||||
.max_width(allocate_width)
|
.max_width(allocate_width)
|
||||||
.sense(sense)
|
.sense(sense)
|
||||||
.frame(frame)
|
.frame(frame)
|
||||||
.align2(Align2::LEFT_TOP)
|
.align2(align)
|
||||||
.wrap_mode(wrap_mode)
|
.wrap_mode(wrap_mode)
|
||||||
.allocate(ui);
|
.allocate(ui);
|
||||||
|
|
||||||
@@ -740,16 +744,18 @@ 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(
|
||||||
galley.cursor_from_pos(pointer_pos - inner_rect.min + state.text_offset);
|
pointer_pos - inner_rect.min + state.text_offset + vec2(galley.rect.left(), 0.0),
|
||||||
|
);
|
||||||
|
|
||||||
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(inner_rect.min.to_vec2())
|
let cursor_rect = TSTransform::from_translation(
|
||||||
* cursor_rect(&galley, &cursor_at_pointer, row_height);
|
inner_rect.min.to_vec2() - vec2(galley.rect.left(), 0.0),
|
||||||
|
) * 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -835,7 +841,7 @@ impl TextEdit<'_> {
|
|||||||
|
|
||||||
if has_focus && let Some(cursor_range) = state.cursor.range(&galley) {
|
if has_focus && let Some(cursor_range) = state.cursor.range(&galley) {
|
||||||
let primary_cursor_rect = cursor_rect(&galley, &cursor_range.primary, row_height)
|
let primary_cursor_rect = cursor_rect(&galley, &cursor_range.primary, row_height)
|
||||||
.translate(galley_pos.to_vec2());
|
.translate(galley_pos.to_vec2() - vec2(galley.rect.left(), 0.0));
|
||||||
|
|
||||||
if response.changed() || selection_changed {
|
if response.changed() || selection_changed {
|
||||||
// Scroll to keep primary cursor in view:
|
// Scroll to keep primary cursor in view:
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
|
use egui::{Align, Align2, AtomExt as _};
|
||||||
|
|
||||||
/// Showcase [`egui::TextEdit`].
|
/// Showcase [`egui::TextEdit`].
|
||||||
#[derive(PartialEq, Eq)]
|
#[derive(PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
#[cfg_attr(feature = "serde", serde(default))]
|
#[cfg_attr(feature = "serde", serde(default))]
|
||||||
pub struct TextEditDemo {
|
pub struct TextEditDemo {
|
||||||
pub text: String,
|
pub text: String,
|
||||||
|
halign: egui::Align,
|
||||||
|
valign: egui::Align,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TextEditDemo {
|
impl Default for TextEditDemo {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
text: "Edit this text".to_owned(),
|
text: "Edit this text".to_owned(),
|
||||||
|
halign: egui::Align::LEFT,
|
||||||
|
valign: egui::Align::TOP,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,7 +43,11 @@ impl crate::View for TextEditDemo {
|
|||||||
ui.add(crate::egui_github_link_file!());
|
ui.add(crate::egui_github_link_file!());
|
||||||
});
|
});
|
||||||
|
|
||||||
let Self { text } = self;
|
let Self {
|
||||||
|
text,
|
||||||
|
halign,
|
||||||
|
valign,
|
||||||
|
} = self;
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.spacing_mut().item_spacing.x = 0.0;
|
ui.spacing_mut().item_spacing.x = 0.0;
|
||||||
@@ -46,10 +56,40 @@ impl crate::View for TextEditDemo {
|
|||||||
ui.label(".");
|
ui.label(".");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Horizontal align:");
|
||||||
|
ui.selectable_value(halign, egui::Align::LEFT, "Left");
|
||||||
|
ui.selectable_value(halign, egui::Align::Center, "Center");
|
||||||
|
ui.selectable_value(halign, egui::Align::RIGHT, "Right");
|
||||||
|
});
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Vertical align:");
|
||||||
|
ui.selectable_value(valign, egui::Align::TOP, "Top");
|
||||||
|
ui.selectable_value(valign, egui::Align::Center, "Center");
|
||||||
|
ui.selectable_value(valign, egui::Align::BOTTOM, "Bottom");
|
||||||
|
});
|
||||||
|
|
||||||
|
let clear_id = egui::Id::new("clear_button");
|
||||||
|
let clear_size = egui::Vec2::splat(ui.spacing().interact_size.y);
|
||||||
|
|
||||||
let output = egui::TextEdit::multiline(text)
|
let output = egui::TextEdit::multiline(text)
|
||||||
.hint_text("Type something!")
|
.hint_text("Type something!")
|
||||||
|
// Atoms are centered by default, so we need to pass the right align here:
|
||||||
|
.prefix("🔎".atom_align(Align2([Align::LEFT, *valign])))
|
||||||
|
.suffix(
|
||||||
|
egui::Atom::custom(clear_id, clear_size)
|
||||||
|
.atom_align(Align2([Align::RIGHT, *valign])),
|
||||||
|
)
|
||||||
|
.horizontal_align(*halign)
|
||||||
|
.vertical_align(*valign)
|
||||||
.show(ui);
|
.show(ui);
|
||||||
|
|
||||||
|
if let Some(rect) = output.response.rect(clear_id)
|
||||||
|
&& ui.place(rect, egui::Button::new("❌")).clicked()
|
||||||
|
{
|
||||||
|
text.clear();
|
||||||
|
}
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.spacing_mut().item_spacing.x = 0.0;
|
ui.spacing_mut().item_spacing.x = 0.0;
|
||||||
ui.label("Selected text: ");
|
ui.label("Selected text: ");
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9c09529c3a1c26c8f28c00fc15cc5f495842862276870c24b5ee0713954f97fc
|
oid sha256:94c4af5715992f4dbb5bbec6ce67eec1e2f66cfc078a3e704ec386bdb482cac4
|
||||||
size 21916
|
size 30064
|
||||||
|
|||||||
@@ -1244,7 +1244,8 @@ impl Galley {
|
|||||||
|
|
||||||
let new_layout_cursor = {
|
let new_layout_cursor = {
|
||||||
// keep same X coord
|
// keep same X coord
|
||||||
let column = self.rows[new_row].char_at(h_pos);
|
// char_at is Row-relative, so subtract the row's position
|
||||||
|
let column = self.rows[new_row].char_at(h_pos - self.rows[new_row].pos.x);
|
||||||
LayoutCursor {
|
LayoutCursor {
|
||||||
row: new_row,
|
row: new_row,
|
||||||
column,
|
column,
|
||||||
@@ -1266,7 +1267,8 @@ impl Galley {
|
|||||||
|
|
||||||
let new_layout_cursor = {
|
let new_layout_cursor = {
|
||||||
// keep same X coord
|
// keep same X coord
|
||||||
let column = self.rows[new_row].char_at(h_pos);
|
// char_at is Row-relative, so subtract the row's position
|
||||||
|
let column = self.rows[new_row].char_at(h_pos - self.rows[new_row].pos.x);
|
||||||
LayoutCursor {
|
LayoutCursor {
|
||||||
row: new_row,
|
row: new_row,
|
||||||
column,
|
column,
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a40004fe56075f31162e16c7c59c00d7e1b8132bbea603b3c54c4dec0875b1bb
|
oid sha256:a9f298f8ea6692e7ccbddbe182a91824ce262913d50e9a7df104a6c63d39d8a0
|
||||||
size 364491
|
size 372564
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f567547c446ffa75f968e0ffc505560f3b3d4171319fbe59be27dde4e553e287
|
oid sha256:48114ad6d116fb9288ce9fe3b173017bda317f69753e7ac03a090b8d02d6cb4d
|
||||||
size 13273
|
size 13258
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e11dbb1d48a3eadecb5c0e36917785fa1f107e4e283ff2f76831482fe7cd2042
|
oid sha256:a37ed30425967301ffa7dda3fdc8f316dfd7f4665c731b17778e3e5942783e81
|
||||||
size 10051
|
size 10534
|
||||||
|
|||||||
Reference in New Issue
Block a user