Number fields take a decimal comma too (#1347) (#1418)

A number field reads "2,5" as 2.5, as a numeric keypad types it in many locales; a comma beside a point or a second comma is not guessed at.
This commit is contained in:
bherbruck authored and GitHub committed 2026-10-15 10:32:14 -04:00
1 parent 4c7ea34a68
commit 8b97b9ed62
5 files changed
+38 -7

No files matched your search

+2 -2
View File
@@ -1155,7 +1155,7 @@ fn canvas_panel(app: &mut SolveApp, ctx: &egui::Context) {
let r3 = ui.add(crate::prefs::length_drag(&mut w, &app.session.doc.units, app.preferences.length_decimals).range(0.01..=1e6));
ui.end_row();
ui.label(crate::i18n::tr_label("Angle"));
let r4 = ui.add(egui::DragValue::new(&mut ang).speed(1.0).suffix(" deg"));
let r4 = ui.add(egui::DragValue::new(&mut ang).speed(1.0).suffix(" deg").custom_parser(crate::prefs::parse_number));
ui.end_row();
ui.label(crate::i18n::tr_label("Opacity"));
let r5 = ui.add(egui::Slider::new(&mut op, 0.0..=1.0));
@@ -1721,7 +1721,7 @@ fn occurrence_panel(app: &mut SolveApp, ui: &mut egui::Ui) {
for (k, axis) in ["X angle", "Y angle", "Z angle"].iter().enumerate() {
ui.label(*axis);
if let Some(v) = m.angles.get_mut(k) {
let response = ui.add(egui::DragValue::new(v).speed(1.0).suffix(" deg"));
let response = ui.add(egui::DragValue::new(v).speed(1.0).suffix(" deg").custom_parser(crate::prefs::parse_number));
crate::scenario::publish_handle(&format!("occurrence-move:angle:{k}"), response.rect.center());
}
ui.end_row();
+8 -2
View File
@@ -2374,8 +2374,14 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
for (k, (t, r)) in mids.iter_mut().enumerate() {
row_label(ui, "Mid Radius");
ui.horizontal(|ui| {
ui.add(egui::DragValue::new(t).speed(0.01).range(0.01..=0.99).fixed_decimals(2))
.on_hover_text(tr("Position along the edge, 0 at the start end, 1 at the other"));
ui.add(
egui::DragValue::new(t)
.speed(0.01)
.range(0.01..=0.99)
.fixed_decimals(2)
.custom_parser(crate::prefs::parse_number),
)
.on_hover_text(tr("Position along the edge, 0 at the start end, 1 at the other"));
enter |= field(ui, r);
if ui.small_button("×").on_hover_text(tr("Remove this point")).clicked() {
remove = Some(k);
+1 -1
View File
@@ -390,7 +390,7 @@ pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Ap, inputs: &mut [Sel
("appearance.dielectric_ior", "appearance:ior", &mut ior, 1.0..=3.0),
] {
row_label(ui, app.tr(label));
let response = ui.add(egui::DragValue::new(value).range(range).speed(0.01));
let response = ui.add(egui::DragValue::new(value).range(range).speed(0.01).custom_parser(crate::prefs::parse_number));
crate::scenario::publish_handle(handle, response.rect.center());
if response.changed() {
k.remove_override = false;
+3 -1
View File
@@ -408,7 +408,7 @@ pub fn label(k: &ConstraintKind, expr: Option<&str>, driven: bool, units: &str,
let plain = expr.is_none_or(|e| {
let e = e.trim();
let e = ["mm", "cm", "in", "ft", "m", "deg", "rad", "°"].iter().find_map(|unit| e.strip_suffix(unit)).unwrap_or(e);
e.trim().parse::<f64>().is_ok()
crate::prefs::parse_number(e).is_some()
});
let text = if plain { num } else { format!("fx: {num}") };
if driven { format!("({text})") } else { text }
@@ -823,6 +823,8 @@ mod tests {
let k = ConstraintKind::Length { l: 0, value: 25.0 };
assert_eq!(label(&k, Some("25 mm"), false, "mm", 2), "25.00");
assert_eq!(label(&k, Some("width * 2"), false, "mm", 2), "fx: 25.00");
// A value typed with a decimal comma is a plain value too (#1347).
assert_eq!(label(&ConstraintKind::Length { l: 0, value: 25.5 }, Some("25,5 mm"), false, "mm", 2), "25.50");
assert_eq!(label(&k, None, true, "mm", 2), "(25.00)");
assert_eq!(label(&ConstraintKind::Radius { c: 0, value: 5.0 }, Some("5 mm"), false, "mm", 2), "R5.00");
assert_eq!(label(&ConstraintKind::Diameter { c: 0, value: 10.0 }, Some("10"), false, "mm", 2), "Ø10.00");
+24 -1
View File
@@ -683,6 +683,16 @@ pub fn length_number(mm: f64, units: &str, decimals: u8) -> String {
format!("{:.*}", length_precision(value, units, decimals), value)
}
/// A plain number typed into a field, with a decimal point or a decimal comma (#1347): "2.5"
/// and "2,5" are both 2.5, as a numeric keypad types either depending on the locale. Spaces are
/// ignored and "−" is a minus, as in egui's own parser. A comma beside a point ("1.234,5") or a
/// second comma is not guessed at: the text is not a number.
pub fn parse_number(text: &str) -> Option<f64> {
let t: String = text.chars().filter(|c| !c.is_whitespace()).map(|c| if c == '\u{2212}' { '-' } else { c }).collect();
let t = if !t.contains('.') && t.matches(',').count() == 1 { t.replacen(',', ".", 1) } else { t };
t.parse::<f64>().ok().filter(|v| v.is_finite())
}
/// Edit a millimetre value through a control expressed in the current design units.
/// Keep the stored value untouched unless the user actually changes the field.
pub fn length_drag<'a>(value: &'a mut f64, units: &str, decimals: u8) -> egui::DragValue<'a> {
@@ -1001,7 +1011,7 @@ fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
});
ui.end_row();
ui.label(app.tr("ui.autosave_every"));
if ui.add(egui::DragValue::new(&mut app.autosave_minutes).range(0.0..=120.0).speed(0.5).suffix(" min")).changed() {
if ui.add(egui::DragValue::new(&mut app.autosave_minutes).range(0.0..=120.0).speed(0.5).suffix(" min").custom_parser(parse_number)).changed() {
set_autosave(app);
}
ui.end_row();
@@ -1438,6 +1448,19 @@ pub fn set(app: &mut SolveApp, p: &serde_json::Value) -> Result<serde_json::Valu
#[cfg(test)]
mod tests {
#[test]
fn number_fields_take_a_decimal_point_or_a_decimal_comma() {
// #1347
assert_eq!(super::parse_number("2.5"), Some(2.5));
assert_eq!(super::parse_number("2,5"), Some(2.5));
assert_eq!(super::parse_number(" -0,25 "), Some(-0.25));
assert_eq!(super::parse_number("\u{2212}1,5"), Some(-1.5));
assert_eq!(super::parse_number("12"), Some(12.0));
for bad in ["1.234,5", "1,2,3", ",", "abc", "", "inf", "NaN"] {
assert_eq!(super::parse_number(bad), None, "{bad}");
}
}
/// Display quality (#1287) is a preference like any other: set by name, checked, saved with
/// the others, back to Normal with Restore Defaults.
#[test]