From dcd0c72d53695068e9ba2627c726aaa9b6c3ae2c Mon Sep 17 00:00:00 2001 From: Davy <95214375+thedavidweng@users.noreply.github.com> Date: Mon, 3 Aug 2026 01:55:18 -0700 Subject: [PATCH] Fix TextEdit hint text not following horizontal_align/vertical_align (#8332) ## Summary - Closes #8309 - [x] I have followed the instructions in the PR template `TextEdit` hint text was always aligned to `Align2::LEFT_TOP`, ignoring the alignment set via `TextEdit::horizontal_align` / `vertical_align`. This caused the hint text, the cursor, and the typed text to disagree on alignment: e.g. a centered `TextEdit` showed a left-aligned hint with a centered cursor. The hint text atoms now use the widget's `align`, so the hint matches the input text alignment. The default `align` is still `LEFT_TOP`, so multi line text edits (and the default styling) are unchanged. ### Root cause In `crates/egui/src/widgets/text_edit/builder.rs`, the hint-text branch hardcoded: ```rust atoms.push_right(atom.atom_align(Align2::LEFT_TOP)); ``` while the input-text branch used `.atom_align(self.align)`. The hint path now uses `align` as well. ### Drive-by: silence `clippy::unnecessary_wraps` in `egui_kittest::app_kind` `AppKind::run` returns `Option`. The `Option` wrap is required when the `eframe` feature is enabled (the `Eframe` branch returns `None`), but `clippy::unnecessary_wraps` fires when `egui_kittest` is built standalone without the `eframe` feature (e.g. `cargo clippy -p egui_kittest`). The workspace CI run doesn't hit it because feature unification via `egui_demo_app` enables `eframe`, but it's a real annoyance for anyone linting the crate on its own. Added a scoped `#[cfg_attr(not(feature = "eframe"), expect(clippy::unnecessary_wraps))]` with an explanatory comment. ## Test plan - [x] Added `textedit_hint_text_should_follow_text_alignment` kittest regression in `crates/egui_kittest/tests/regression_tests.rs`. It fails before the fix (`hint_center_x=24.25` vs `edit_center_x=100`) and passes after. - [x] `cargo test -p egui` - [x] `cargo test -p egui_kittest --all-features --test regression_tests` - [x] `cargo clippy -p egui_kittest --all-features --test regression_tests -- -D warnings` - [x] `RUSTFLAGS="-D warnings" cargo clippy -p egui_kittest --lib` (pre-existing `unnecessary_wraps` now silenced) - [x] `cargo clippy -p egui -- -D warnings` - [x] `cargo fmt --check` --------- Co-authored-by: Lucas Meurer --- crates/egui/src/widgets/text_edit/builder.rs | 7 +-- crates/egui_kittest/src/app_kind.rs | 4 ++ crates/egui_kittest/tests/regression_tests.rs | 46 +++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 0a52d636c..aa4f18d8a 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -622,9 +622,10 @@ impl TextEdit<'_> { 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)); + // Align the hint text the same as the input text so the hint, the + // cursor, and the typed text all share one alignment. The default + // `align` is `LEFT_TOP`, which keeps multi line text edits unchanged. + atoms.push_right(atom.atom_align(align)); } // Calculate the empty galley, so it can be read later. The available width is diff --git a/crates/egui_kittest/src/app_kind.rs b/crates/egui_kittest/src/app_kind.rs index 942ec4b85..9e92192bb 100644 --- a/crates/egui_kittest/src/app_kind.rs +++ b/crates/egui_kittest/src/app_kind.rs @@ -23,6 +23,10 @@ pub(crate) enum AppKind<'a, State> { } impl AppKind<'_, State> { + // The `Option` is needed when the `eframe` feature is enabled, because the + // `Eframe` variant has no `egui::Response` to return. Without `eframe` the + // wrap is unnecessary, so we silence `clippy::unnecessary_wraps` for that case. + #[cfg_attr(not(feature = "eframe"), expect(clippy::unnecessary_wraps))] pub fn run( &mut self, ui: &mut egui::Ui, diff --git a/crates/egui_kittest/tests/regression_tests.rs b/crates/egui_kittest/tests/regression_tests.rs index ba39a909b..459e2f024 100644 --- a/crates/egui_kittest/tests/regression_tests.rs +++ b/crates/egui_kittest/tests/regression_tests.rs @@ -713,3 +713,49 @@ fn collapsing_panel_must_not_grow_enclosing_window() { ); } } + +/// The hint text of a `TextEdit` should follow the same alignment as the input +/// text, instead of always being left-top aligned. +/// +/// Regression test for . +#[test] +pub fn textedit_hint_text_should_follow_text_alignment() { + let mut input = String::new(); + + let mut harness = Harness::builder() + .with_size(Vec2::new(200.0, 40.0)) + .build_ui(|ui| { + ui.add( + egui::TextEdit::singleline(&mut input) + .hint_text("Hint") + .desired_width(200.0) + .horizontal_align(egui::Align::Center), + ); + }); + harness.run(); + + let text_edit = harness.get_by_role(accesskit::Role::TextInput); + let edit_rect = text_edit.rect(); + + // Find the hint text shape (the only text shape while the input is empty). + let hint_shape = harness + .output() + .shapes + .iter() + .find_map(|clipped| { + let egui::epaint::Shape::Text(text_shape) = &clipped.shape else { + return None; + }; + (text_shape.galley.text() == "Hint").then_some(text_shape) + }) + .expect("hint text shape should be painted"); + + let hint_center_x = hint_shape.pos.x + hint_shape.galley.size().x / 2.0; + let edit_center_x = edit_rect.center().x; + + assert!( + (hint_center_x - edit_center_x).abs() < 1.0, + "hint text should be centered in the TextEdit: hint_center_x={hint_center_x}, \ + edit_center_x={edit_center_x}, edit_rect={edit_rect:?}", + ); +}