From 2a03ae1348ad7473452672d37f1a74bcf3bda3c9 Mon Sep 17 00:00:00 2001 From: RndUsr123 <150948884+RndUsr123@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:35:37 +0000 Subject: [PATCH] Enables every combination of `TextEdit` and `LayoutJob` alignments (#7831) This is a fix/improvement that makes all kinds of alignments work in `TextEdit` when a custom `LayoutJob` with halign is used. I used the simplest approach possible to avoid unwanted bugs as I wasn't sure what's safe to change, but there's potentially better ways to achieve this. In particular, I'm not sure I fully understand the rationale behind aligning rows in a `Galley` based on the latter's leftmost border, considering the size is what's ultimately used in widgets (in `TextEdit` at least). Regardless, here's a demo of this PR: https://github.com/user-attachments/assets/5d9801d7-73af-4576-80c5-47f169700462 * [x] I have followed the instructions in the PR template --------- Co-authored-by: Emil Ernerfeldt --- crates/egui/src/widgets/text_edit/builder.rs | 6 ++- crates/epaint/src/text/text_layout_types.rs | 4 +- tests/egui_tests/tests/regression_tests.rs | 54 ++++++++++++++++++- .../tests/snapshots/text_edit_halign.png | 3 ++ 4 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 tests/egui_tests/tests/snapshots/text_edit_halign.png diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 086cf3091..a6c41e71c 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -820,7 +820,11 @@ impl TextEdit<'_> { paint_text_selection(&mut galley, ui.visuals(), &cursor_range, None); } - painter.galley(galley_pos, Arc::clone(&galley), text_color); + painter.galley( + galley_pos - vec2(galley.rect.left(), 0.0), + Arc::clone(&galley), + text_color, + ); if has_focus && let Some(cursor_range) = state.cursor.range(&galley) { let primary_cursor_rect = cursor_rect(&galley, &cursor_range.primary, row_height) diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index d887fb13a..22fb03c57 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -1047,7 +1047,7 @@ impl Galley { return self.end_pos(); }; - let x = row.x_offset(layout_cursor.column); + let x = row.x_offset(layout_cursor.column) + row.pos.x - self.rect.left(); Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y())) } @@ -1092,7 +1092,7 @@ impl Galley { if is_pos_within_row || y_dist < best_y_dist { best_y_dist = y_dist; // char_at is `Row` not `PlacedRow` relative which means we have to subtract the pos. - let column = row.char_at(pos.x - row.pos.x); + let column = row.char_at(pos.x - row.pos.x + self.rect.left()); let prefer_next_row = column < row.char_count_excluding_newline(); cursor = CCursor { index: ccursor_index + column, diff --git a/tests/egui_tests/tests/regression_tests.rs b/tests/egui_tests/tests/regression_tests.rs index d61b76af3..d92a77103 100644 --- a/tests/egui_tests/tests/regression_tests.rs +++ b/tests/egui_tests/tests/regression_tests.rs @@ -1,8 +1,13 @@ +use std::sync::Arc; + +use egui::ScrollArea; use egui::accesskit::Role; use egui::epaint::Shape; use egui::style::ScrollAnimation; +use egui::text::{LayoutJob, TextWrapping}; use egui::{ - Align, Color32, Image, Label, Layout, RichText, ScrollArea, Sense, TextWrapMode, include_image, + Align, Color32, FontFamily, FontId, Image, Label, Layout, RichText, Sense, TextBuffer, + TextFormat, TextWrapMode, Ui, include_image, vec2, }; use egui_kittest::Harness; use egui_kittest::kittest::Queryable as _; @@ -66,6 +71,53 @@ fn text_edit_rtl() { } } +#[test] +fn text_edit_halign() { + let mut harness = Harness::builder().with_size((212.0, 212.0)).build_ui(|ui| { + ui.spacing_mut().item_spacing = vec2(2.0, 2.0); + + fn layouter(halign: Align) -> impl FnMut(&Ui, &dyn TextBuffer, f32) -> Arc { + move |ui: &egui::Ui, buf: &dyn egui::TextBuffer, wrap_width: f32| { + let mut job = LayoutJob { + wrap: TextWrapping { + max_rows: 4, + max_width: wrap_width, + ..Default::default() + }, + halign, + ..Default::default() + }; + job.append( + buf.as_str(), + 0.0, + TextFormat::simple(FontId::new(13.0, FontFamily::Proportional), Color32::GRAY), + ); + ui.fonts_mut(|f| f.layout_job(job)) + } + } + + for widget_alignment in [Align::Min, Align::Center, Align::Max] { + ui.horizontal(|ui| { + for text_alignment in [Align::LEFT, Align::Center, Align::RIGHT] { + ui.add_sized( + vec2(64.0, 64.0), + egui::TextEdit::multiline(&mut format!( + "{widget_alignment:?}\n+\n{text_alignment:?}", + )) + .layouter(&mut layouter(text_alignment)) + .vertical_align(widget_alignment) + .horizontal_align(widget_alignment), + ); + } + }); + } + }); + + harness.get_by_value("Center\n+\nCenter").focus(); + harness.step(); + harness.snapshot("text_edit_halign"); +} + #[test] fn text_edit_delay() { let mut text = String::new(); diff --git a/tests/egui_tests/tests/snapshots/text_edit_halign.png b/tests/egui_tests/tests/snapshots/text_edit_halign.png new file mode 100644 index 000000000..29546a036 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/text_edit_halign.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:502607c803b884e4e1640d39c97b03b0a40df93c2da328f889168e386f837f36 +size 13261