Fix #4567: vertically center numbers and text in input fields

In single-line text and numeric inputs across panels and dialogs, egui
0.37 defaulted TextEdit alignment to Align2::LEFT_TOP, placing the text
galley at the very top of the allocated input box (e.g. 1pt top margin,
11pt bottom margin in 26pt rows).

Align single-line TextEdit instances to Align2::LEFT_CENTER across
recessed_text, opt_field_named (dash/gap cells), hex fields, search
fields, and dialog inputs, matching DesignCraft's vertical centering.
Add regression assertions in look_tests verifying balanced vertical
spacing.
This commit is contained in:
el-j
2026-10-29 19:21:33 +07:00
committed by bflatastic
parent cdf4453051
commit 752bbb050d
24 changed files with 130 additions and 27 deletions
+5 -1
View File
@@ -131,7 +131,11 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog) -> bool {
ui.horizontal(|ui| {
widgets::label_row(ui, tl!("Scale:"), 96.0, |ui| {
let mut scale = f.get("scale").and_then(Value::as_str).unwrap_or_default().to_owned();
let changed = ui.name_inputs(tl!("Scale"), |ui| ui.add(egui::TextEdit::singleline(&mut scale).desired_width(72.0))).changed();
let changed = ui
.name_inputs(tl!("Scale"), |ui| {
ui.add(egui::TextEdit::singleline(&mut scale).align(egui::Align2::LEFT_CENTER).desired_width(72.0))
})
.changed();
if changed {
f.insert("scale".into(), json!(scale));
}
+1 -1
View File
@@ -103,7 +103,7 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
d.fields.insert("all".into(), json!(all));
}
let mut range = d.str("range");
if ui.add_enabled(!all, egui::TextEdit::singleline(&mut range).desired_width(90.0)).changed() {
if ui.add_enabled(!all, egui::TextEdit::singleline(&mut range).align(egui::Align2::LEFT_CENTER).desired_width(90.0)).changed() {
d.fields.insert("range".into(), json!(range));
}
});
+9 -2
View File
@@ -181,7 +181,14 @@ fn text_edit_limited_name(ui: &mut egui::Ui, d: &mut Dialog, key: &str, width: f
.inner_margin(egui::Margin::symmetric(6, 3))
.show(ui, |ui| {
crate::theme::strong_selected_text(ui);
ui.add(egui::TextEdit::singleline(&mut s).id(id).frame(egui::Frame::NONE).desired_width(width).char_limit(max_chars))
ui.add(
egui::TextEdit::singleline(&mut s)
.id(id)
.frame(egui::Frame::NONE)
.align(egui::Align2::LEFT_CENTER)
.desired_width(width)
.char_limit(max_chars),
)
});
if r.inner.changed() {
d.fields.insert(key.into(), Value::String(s));
@@ -404,7 +411,7 @@ pub(super) fn param_fields(
}
}
Value::String(mut s) => {
if ui.add(egui::TextEdit::singleline(&mut s).desired_width(140.0)).changed() {
if ui.add(egui::TextEdit::singleline(&mut s).align(egui::Align2::LEFT_CENTER).desired_width(140.0)).changed() {
d.fields.insert(k.clone(), json!(s));
changed = true;
}
+1 -1
View File
@@ -379,7 +379,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
let mut text = d.str("entry");
// The entry box has the keys when the window opens, so Enter moves down instead of applying.
widgets::take_dialog_focus(ui, entry_id, &text);
let r = ui.add(egui::TextEdit::singleline(&mut text).id(entry_id).desired_width(260.0));
let r = ui.add(egui::TextEdit::singleline(&mut text).id(entry_id).align(egui::Align2::LEFT_CENTER).desired_width(260.0));
if r.changed() {
d.fields.insert("entry".into(), json!(clean(&text)));
}
+1 -1
View File
@@ -297,7 +297,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
fn password_row(ui: &mut egui::Ui, d: &mut Dialog) {
widgets::label_row(ui, tl!("Password"), LABEL_WIDTH, |ui| {
let mut s = d.str("password");
let r = ui.add(egui::TextEdit::singleline(&mut s).password(true).desired_width(180.0));
let r = ui.add(egui::TextEdit::singleline(&mut s).password(true).align(egui::Align2::LEFT_CENTER).desired_width(180.0));
if r.changed() {
d.fields.insert("password".into(), json!(s));
}
+1 -1
View File
@@ -582,7 +582,7 @@ fn general(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
}
}
let mut range = get(d, "range").as_str().unwrap_or_default().to_string();
let edit = egui::TextEdit::singleline(&mut range).desired_width(140.0).hint_text("1-3, 5");
let edit = egui::TextEdit::singleline(&mut range).align(egui::Align2::LEFT_CENTER).desired_width(140.0).hint_text("1-3, 5");
if ui.add_enabled(artboards == PrintArtboards::Range, edit).changed() {
set(d, "range", json!(range));
}
+1 -1
View File
@@ -124,7 +124,7 @@ fn artboard_range(ui: &mut egui::Ui, d: &mut Dialog) {
d.fields.insert("all".into(), json!(all));
}
let mut range = d.str("range");
let field = egui::TextEdit::singleline(&mut range).desired_width(90.0).hint_text("1-3, 5");
let field = egui::TextEdit::singleline(&mut range).align(egui::Align2::LEFT_CENTER).desired_width(90.0).hint_text("1-3, 5");
if ui.add_enabled(!all, field).changed() {
d.fields.insert("range".into(), json!(range));
}
+6 -3
View File
@@ -321,7 +321,7 @@ pub(super) fn length(ui: &mut egui::Ui, d: &mut Dialog, path: &str, unit: Unit,
pub(super) fn text(ui: &mut egui::Ui, d: &mut Dialog, path: &str, enabled: bool) {
let mut s = get(d, path).as_str().unwrap_or_default().to_string();
if ui.add_enabled(enabled, egui::TextEdit::singleline(&mut s).desired_width(240.0)).changed() {
if ui.add_enabled(enabled, egui::TextEdit::singleline(&mut s).align(egui::Align2::LEFT_CENTER).desired_width(240.0)).changed() {
set(d, path, json!(s));
}
}
@@ -493,7 +493,10 @@ fn general(ui: &mut egui::Ui, d: &mut Dialog, editor: bool) {
d.fields.insert("__allArtboards".into(), json!(all));
}
let mut range = d.str("range");
if ui.add_enabled(!all, egui::TextEdit::singleline(&mut range).desired_width(140.0).hint_text("1-3, 5")).changed() {
if ui
.add_enabled(!all, egui::TextEdit::singleline(&mut range).align(egui::Align2::LEFT_CENTER).desired_width(140.0).hint_text("1-3, 5"))
.changed()
{
d.fields.insert("range".into(), json!(range));
}
});
@@ -700,7 +703,7 @@ fn password_check(ui: &mut egui::Ui, d: &mut Dialog, path: &str, flag: &str, lab
fn password_field(ui: &mut egui::Ui, d: &mut Dialog, path: &str, enabled: bool) {
let mut s = get(d, path).as_str().unwrap_or_default().to_string();
if ui.add_enabled(enabled, egui::TextEdit::singleline(&mut s).password(true).desired_width(200.0)).changed() {
if ui.add_enabled(enabled, egui::TextEdit::singleline(&mut s).password(true).align(egui::Align2::LEFT_CENTER).desired_width(200.0)).changed() {
set(d, path, json!(s));
}
}
@@ -667,7 +667,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
ui.horizontal(|ui| {
ui.label(egui::RichText::new(tl!("Name:")).color(t.text_dim));
let mut name = d.str("name");
if ui.add(egui::TextEdit::singleline(&mut name).desired_width(260.0)).changed() {
if ui.add(egui::TextEdit::singleline(&mut name).align(egui::Align2::LEFT_CENTER).desired_width(260.0)).changed() {
d.fields.insert("name".into(), json!(name));
}
});
@@ -867,7 +867,7 @@ fn rows_section(ui: &mut egui::Ui, d: &mut Dialog, review: &mut Review, statuses
}
ui.add_space(8.0);
let mut q = d.str("__query");
if ui.add(egui::TextEdit::singleline(&mut q).hint_text(tl!("Search")).desired_width(180.0)).changed() {
if ui.add(egui::TextEdit::singleline(&mut q).align(egui::Align2::LEFT_CENTER).hint_text(tl!("Search")).desired_width(180.0)).changed() {
d.fields.insert("__query".into(), json!(q));
}
});
+1 -1
View File
@@ -245,7 +245,7 @@ fn artboards(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
d.fields.insert("allArtboards".into(), json!(false));
}
let mut range = d.str("range");
let edit = egui::TextEdit::singleline(&mut range).hint_text(format!("1-{count}")).desired_width(90.0);
let edit = egui::TextEdit::singleline(&mut range).align(egui::Align2::LEFT_CENTER).hint_text(format!("1-{count}")).desired_width(90.0);
if ui.add_enabled(!all, edit).changed() {
d.fields.insert("range".into(), json!(range));
}
+8 -1
View File
@@ -266,7 +266,14 @@ fn hex_field(ui: &mut egui::Ui, hex: &str) -> Option<Color> {
.inner_margin(egui::Margin::symmetric(6, 4))
.show(ui, |ui| {
crate::theme::strong_selected_text(ui);
ui.add(egui::TextEdit::singleline(&mut buf).id(id).frame(egui::Frame::NONE).desired_width(70.0).char_limit(7))
ui.add(
egui::TextEdit::singleline(&mut buf)
.id(id)
.frame(egui::Frame::NONE)
.align(egui::Align2::LEFT_CENTER)
.desired_width(70.0)
.char_limit(7),
)
})
.inner;
let focused = ui.memory(|m| m.has_focus(id));
+1 -1
View File
@@ -225,7 +225,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
/// A text field for `key`, written back as it is typed.
fn text(ui: &mut egui::Ui, d: &mut Dialog, key: &str, width: f32) -> egui::Response {
let mut s = d.str(key);
let r = ui.add(egui::TextEdit::singleline(&mut s).desired_width(width));
let r = ui.add(egui::TextEdit::singleline(&mut s).align(egui::Align2::LEFT_CENTER).desired_width(width));
if r.changed() {
d.fields.insert(key.into(), json!(s));
}
+6 -1
View File
@@ -104,7 +104,12 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
egui::Area::new(egui::Id::unique("palette")).order(egui::Order::Foreground).anchor(egui::Align2::CENTER_TOP, [0.0, 90.0]).show(ctx, |ui| {
egui::Frame::popup(ui.style()).fill(t.panel).inner_margin(egui::Margin::same(10)).show(ui, |ui| {
ui.set_width(520.0);
let r = ui.add(egui::TextEdit::singleline(&mut app.ui.palette_query).hint_text(tl!("Search commands and tools…")).desired_width(500.0));
let r = ui.add(
egui::TextEdit::singleline(&mut app.ui.palette_query)
.align(egui::Align2::LEFT_CENTER)
.hint_text(tl!("Search commands and tools…"))
.desired_width(500.0),
);
r.request_focus();
ui.add_space(6.0);
for (i, e) in matches.iter().enumerate() {
+4 -1
View File
@@ -437,7 +437,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let id = ui.scope_id().with(("ab-name", i));
let mut buf: String = ui.data_mut(|d| d.get_temp::<String>(id)).unwrap_or_else(|| shown.to_string());
let kept = widgets::own_field_shift_enter(ui, id, &mut buf, shown);
let te = ui.put(name_rect, egui::TextEdit::singleline(&mut buf).id(id).font(egui::FontId::proportional(12.0)));
let te = ui.put(
name_rect,
egui::TextEdit::singleline(&mut buf).id(id).align(egui::Align2::LEFT_CENTER).font(egui::FontId::proportional(12.0)),
);
if !te.has_focus() && !te.lost_focus() {
te.request_focus();
}
+7 -2
View File
@@ -130,8 +130,13 @@ fn tiles(ui: &mut Ui, st: &DocState, sel: &[u64], ev: &mut Events) {
let field = rename_id.with("field");
let kept = widgets::own_field_shift_enter(ui, field, &mut buf, &a.name);
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(name_rect));
let te = child
.add(egui::TextEdit::singleline(&mut buf).id(field).desired_width(name_rect.width()).font(egui::FontId::proportional(11.5)));
let te = child.add(
egui::TextEdit::singleline(&mut buf)
.id(field)
.align(egui::Align2::LEFT_CENTER)
.desired_width(name_rect.width())
.font(egui::FontId::proportional(11.5)),
);
// Enter or a click elsewhere commits, Escape cancels; Shift+Enter commits and
// keeps the field open.
if te.lost_focus() {
+3 -1
View File
@@ -250,7 +250,9 @@ pub(super) fn row(ui: &mut Ui, view: &View, item: &Row, out: &mut Out) {
let field = rename_id().with("field");
let kept = crate::widgets::own_field_shift_enter(ui, field, &mut buf, &name);
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(name_rect));
let te = child.add(egui::TextEdit::singleline(&mut buf).id(field).desired_width(name_rect.width()).font(font.clone()));
let te = child.add(
egui::TextEdit::singleline(&mut buf).id(field).align(egui::Align2::LEFT_CENTER).desired_width(name_rect.width()).font(font.clone()),
);
// Focus the field as it opens. Asking every frame took the focus back from Enter,
// Escape or a click elsewhere, so the rename never ended.
if !te.has_focus() && !te.lost_focus() {
+1 -1
View File
@@ -51,7 +51,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
buf = name.clone();
}
let kept = widgets::own_field_shift_enter(ui, id, &mut buf, &name);
let r = ui.add(egui::TextEdit::singleline(&mut buf).id(id).desired_width(150.0));
let r = ui.add(egui::TextEdit::singleline(&mut buf).id(id).align(egui::Align2::LEFT_CENTER).desired_width(150.0));
ui.data_mut(|d| d.insert_temp(id, buf.clone()));
if (r.lost_focus() || kept) && !buf.trim().is_empty() && buf != name {
set(app, &name, json!({"newName": buf.trim()}));
+1 -1
View File
@@ -371,7 +371,7 @@ fn category_fields(ui: &mut egui::Ui, d: &mut Dialog, cat: &str) {
PrefKind::Text => {
labeled(ui, sp.label, |ui| {
let mut s = v.as_str().unwrap_or("").to_string();
if ui.add(egui::TextEdit::singleline(&mut s).desired_width(260.0)).changed() {
if ui.add(egui::TextEdit::singleline(&mut s).align(egui::Align2::LEFT_CENTER).desired_width(260.0)).changed() {
d.fields.insert(sp.key.into(), json!(s));
}
});
+2 -1
View File
@@ -788,7 +788,8 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let mut q = d.str("query");
if ui.add(egui::TextEdit::singleline(&mut q).hint_text(tl!("Search")).desired_width(200.0)).changed() {
if ui.add(egui::TextEdit::singleline(&mut q).align(egui::Align2::LEFT_CENTER).hint_text(tl!("Search")).desired_width(200.0)).changed()
{
d.fields.insert("query".into(), json!(q));
}
});
+1
View File
@@ -417,6 +417,7 @@ pub fn hex_field_named(ui: &mut Ui, id: impl std::hash::Hash, name: Option<&str>
egui::TextEdit::singleline(&mut buf)
.id(id)
.frame(egui::Frame::NONE)
.align(egui::Align2::LEFT_CENTER)
.desired_width(62.0)
.char_limit(7)
.font(egui::FontId::proportional(12.5))
+59
View File
@@ -99,6 +99,65 @@ fn a_spinner_field_reads_as_one_box_in_three_parts() {
for pair in boxes.windows(2) {
assert!((pair[1].min.x - pair[0].max.x + 1.0).abs() < 0.01, "neighbours share one border: {boxes:?}");
}
// The number text in the numeric field is vertically centered in the row box (#900, #4567).
let text_shape = shapes
.iter()
.find_map(|s| match s {
egui::Shape::Text(t) if t.galley.text() == "4 pt" => Some(t),
_ => None,
})
.expect("field text shape present");
let text_top = text_shape.pos.y - boxes[1].min.y;
let text_bottom = boxes[1].max.y - (text_shape.pos.y + text_shape.galley.rect.height());
assert!((text_top - text_bottom).abs() <= 1.5, "text is vertically centered in its box: top gap {text_top}, bottom gap {text_bottom}");
}
// Regression test for #4567: numbers and text in input fields (numeric fields, dash/gap fields,
// etc.) are vertically centered in their boxes, not shoved against the top edge.
#[test]
fn input_elements_are_vertically_centered_in_their_boxes() {
let ctx = themed(Default::default());
let mut add = |ui: &mut Ui| {
ui.horizontal(|ui| {
num_field(ui, "plain_num", Some(12.0), Unit::Points, 70.0);
opt_field(ui, "dash_cell", Some(12.0), Unit::Points, 40.0);
});
};
frame(&ctx, 0.0, vec![], &mut add);
let shapes = frame(&ctx, 0.1, vec![], &mut add);
let mut boxes: Vec<Rect> = vec![];
for s in &shapes {
if let egui::Shape::Rect(r) = s
&& r.rect.height() > 20.0
&& r.fill != Color32::TRANSPARENT
&& !boxes.contains(&r.rect)
{
boxes.push(r.rect);
}
}
boxes.sort_by(|a, b| a.min.x.total_cmp(&b.min.x));
assert_eq!(boxes.len(), 2, "found both field boxes: {boxes:?}");
let mut text_shapes: Vec<&egui::epaint::TextShape> = vec![];
for s in &shapes {
if let egui::Shape::Text(t) = s
&& (t.galley.text() == "12 pt" || t.galley.text() == "12")
{
text_shapes.push(t);
}
}
text_shapes.sort_by(|a, b| a.pos.x.total_cmp(&b.pos.x));
assert_eq!(text_shapes.len(), 2, "found both text shapes: {text_shapes:?}");
for (b, t) in boxes.iter().zip(text_shapes.iter()) {
let text_top = t.pos.y - b.min.y;
let text_bottom = b.max.y - (t.pos.y + t.galley.rect.height());
assert!(
(text_top - text_bottom).abs() <= 1.5,
"text '{}' is vertically centered: top gap {text_top}, bottom gap {text_bottom} in box {b:?}",
t.galley.text()
);
}
}
/// The colour of every glyph of the text in `shapes`: selected glyphs are drawn in the
+1
View File
@@ -710,6 +710,7 @@ pub fn opt_field_named(ui: &mut Ui, id: impl std::hash::Hash, name: Option<&str>
egui::TextEdit::singleline(&mut buf)
.id(id)
.frame(egui::Frame::NONE)
.align(egui::Align2::LEFT_CENTER)
.desired_width(width - 10.0)
.font(egui::FontId::proportional(12.5))
.text_color(if enabled { t.text_strong } else { t.text_disabled }),
+6 -1
View File
@@ -206,7 +206,11 @@ pub(super) fn recessed_text(
.inner_margin(margin)
.show(ui, |ui| {
crate::theme::strong_selected_text(ui);
let edit = if rows > 1 { egui::TextEdit::multiline(&mut buf).desired_rows(rows) } else { egui::TextEdit::singleline(&mut buf) };
let edit = if rows > 1 {
egui::TextEdit::multiline(&mut buf).desired_rows(rows)
} else {
egui::TextEdit::singleline(&mut buf).align(egui::Align2::LEFT_CENTER)
};
ui.add(
edit.id(id)
.frame(egui::Frame::NONE)
@@ -293,6 +297,7 @@ pub fn search_field(ui: &mut Ui, id: egui::Id, hint: &str) -> String {
egui::TextEdit::singleline(&mut query)
.id(id.with("edit"))
.frame(egui::Frame::NONE)
.align(egui::Align2::LEFT_CENTER)
.hint_text(egui::RichText::new(tl!(hint)).italics())
.desired_width(ui.available_width()),
);
+2 -2
View File
@@ -453,7 +453,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
let sel = d.str("selected");
ui.horizontal(|ui| {
let mut name = d.str("name");
if ui.add(egui::TextEdit::singleline(&mut name).desired_width(180.0)).changed() {
if ui.add(egui::TextEdit::singleline(&mut name).align(egui::Align2::LEFT_CENTER).desired_width(180.0)).changed() {
d.fields.insert("name".into(), json!(name));
}
if ui.button(tl!("New")).on_hover_text(tl!("Save the current layout under this name")).clicked() {
@@ -472,7 +472,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
ui.horizontal(|ui| {
ui.label(egui::RichText::new(tl!("Name:")).color(t.text_dim));
let mut name = d.str("name");
let r = ui.add(egui::TextEdit::singleline(&mut name).desired_width(240.0));
let r = ui.add(egui::TextEdit::singleline(&mut name).align(egui::Align2::LEFT_CENTER).desired_width(240.0));
if r.changed() {
d.fields.insert("name".into(), json!(name));
}