M1: keep sidebar field text in place on focus

This commit is contained in:
Carbocode
2026-10-30 10:44:38 +01:00
parent 84e5278209
commit 0daecb7022
5 changed files with 53 additions and 6 deletions
+2 -1
View File
File diff suppressed because one or more lines are too long
+41
View File
@@ -1153,3 +1153,44 @@ fn prepared_save_cannot_enter_a_replacement(route: &str, save_as: bool, discard:
assert!(u.app.ui.after_field.is_none());
std::fs::remove_dir_all(&dir).ok();
}
/// Replacing a painted value with its editor must preserve its baseline, including inside
/// the horizontal rows used by the sidebar.
#[test]
fn sidebar_field_text_stays_in_place_on_focus() {
for numeric in [true, false] {
let ctx = egui::Context::default();
crate::theme::apply(&ctx, crate::theme::Tokens::light());
let id = Id::unique("baseline_field");
let draw = |events| {
let mut out = ctx.run_ui(
egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(Pos2::ZERO, egui::vec2(400.0, 200.0))), events, ..Default::default() },
|ui| {
ui.horizontal(|ui| {
if numeric {
crate::widgets::number_field(ui, id, crate::widgets::Lead::None, Some(123.0), "", 100.0, 1.0);
} else {
crate::widgets::text_field(ui, id, "123", 100.0, "");
}
});
},
);
out.textures_delta.clear();
out
};
draw(vec![]);
let before = draw(vec![]);
let bounds = |out: &egui::FullOutput| painted_texts(&out.shapes).into_iter().find(|(text, _, _)| text == "123").expect("field text").2;
let before = bounds(&before);
let p = before.center();
for pressed in [true, false] {
draw(vec![
Event::PointerMoved(p),
Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed, modifiers: Modifiers::NONE },
]);
}
assert!(ctx.memory(|m| m.has_focus(id.with("te"))), "editor is focused");
let after = bounds(&draw(vec![]));
assert!((before.center().y - after.center().y).abs() <= 0.5, "numeric={numeric}: before={before:?}, after={after:?}");
}
}
@@ -112,12 +112,13 @@ fn lead_field(ui: &mut Ui, id: Id, glyph: LeadGlyph, value: &str, width: f32, ti
let mut out = None;
match editing {
Some(mut text) => {
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(text_rect.shrink2(vec2(2.0, 4.0))));
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(text_rect.shrink2(vec2(2.0, 0.0))));
let r = child.add(
egui::TextEdit::singleline(&mut text)
.id(id.with("te"))
.frame(egui::Frame::NONE)
.font(regular(11.0))
.align(Align2::LEFT_CENTER)
.desired_width(text_rect.width() - 6.0),
);
crate::a11y::own_focus(&r);
+5 -2
View File
@@ -361,7 +361,9 @@ fn number_field_impl(
.font(regular(11.0))
.desired_width(edit_rect.width() - 6.0)
.align(egui::Align2::LEFT_CENTER);
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(edit_rect.shrink2(vec2(0.0, 4.0))));
// TextEdit already reserves the theme's 24 pt interaction height. Insetting its
// top would move its baseline below the centered value painted while idle.
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(edit_rect));
let r = child.add(te);
a11y::own_focus(&r);
keep_focus(&r, started);
@@ -628,12 +630,13 @@ fn text_field_in(ui: &mut Ui, id: Id, value: &str, rect: Rect, placeholder: &str
/// its own (the field draws it), showing `hint` while empty; `password` shows dots.
fn field_editor(ui: &mut Ui, rect: Rect, id: Id, text: &mut String, hint: &str, password: bool) -> Response {
let t = Tokens::get(ui.ctx());
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(rect.shrink2(vec2(8.0, 4.0))));
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(rect.shrink2(vec2(8.0, 0.0))));
let r = child.add(
egui::TextEdit::singleline(text)
.id(id)
.frame(egui::Frame::NONE)
.password(password)
.align(Align2::LEFT_CENTER)
.font(regular(11.0))
.hint_text(egui::RichText::new(hint).font(regular(11.0)).color(t.text_tertiary))
.desired_width(rect.width() - 16.0),
+3 -2
View File
File diff suppressed because one or more lines are too long