diff --git a/crates/ui-egui/src/browser.rs b/crates/ui-egui/src/browser.rs index 6c396c19..672e2ab6 100644 --- a/crates/ui-egui/src/browser.rs +++ b/crates/ui-egui/src/browser.rs @@ -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(); diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index de221c75..dbf7df3b 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -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); diff --git a/crates/ui-egui/src/dialogs_appearance.rs b/crates/ui-egui/src/dialogs_appearance.rs index b07423cd..765e3329 100644 --- a/crates/ui-egui/src/dialogs_appearance.rs +++ b/crates/ui-egui/src/dialogs_appearance.rs @@ -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; diff --git a/crates/ui-egui/src/dim_view.rs b/crates/ui-egui/src/dim_view.rs index 11bcf39b..1eddb760 100644 --- a/crates/ui-egui/src/dim_view.rs +++ b/crates/ui-egui/src/dim_view.rs @@ -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::().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"); diff --git a/crates/ui-egui/src/prefs.rs b/crates/ui-egui/src/prefs.rs index 21ab2a0a..0bd53b46 100644 --- a/crates/ui-egui/src/prefs.rs +++ b/crates/ui-egui/src/prefs.rs @@ -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 { + 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::().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