From 3e96d59fd8f82fedf9335101cfcbd296675b1d2e Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 4 Nov 2026 10:44:49 -0800 Subject: [PATCH] Numeric fields: 1,000% in a size field is refused A percentage typed alone in a size field (W, H and the other fields that take a share of their value) read its decimal comma before the `%` was dropped. The thousands check then saw `1,000%`, and the comma was read as a decimal point, so `1,000%` set the size to 1% of its value. `share_of` now reads the decimal comma after dropping the `%`: `1,000%` is refused, and `12,5%` still reads as 12.5%. --- crates/ui-egui/src/panels/tests_constrain.rs | 16 ++++++++++++++++ crates/ui-egui/src/widgets.rs | 3 ++- crates/ui-egui/src/widgets/numeric.rs | 8 +++++--- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/panels/tests_constrain.rs b/crates/ui-egui/src/panels/tests_constrain.rs index 1ccf8ac01..8fa4c6e02 100644 --- a/crates/ui-egui/src/panels/tests_constrain.rs +++ b/crates/ui-egui/src/panels/tests_constrain.rs @@ -240,6 +240,22 @@ fn a_percentage_in_a_size_field_scales_the_size_it_shows() { } } +/// A percentage with a thousands comma in W or H (`1,000%`) is refused and leaves the size as it +/// was, rather than read as `1.000%`; a decimal comma (`250,5%`) still reads as a share. +#[test] +fn a_thousands_comma_in_a_percentage_is_refused_in_a_size_field() { + let surfaces: [(&str, Draw); 3] = + [("Transform panel", transform::show), ("Properties", properties::transform_section), ("Control bar", crate::chrome::control_bar)]; + for (name, draw) in surfaces { + let mut app = rect_app(); + set_field(&mut app, draw, "100 pt", "1,000%", End::Enter(Modifiers::NONE)); + assert_eq!(size(&app), (100.0, 50.0), "{name}, 1,000%"); + let mut app = rect_app(); + set_field(&mut app, draw, "100 pt", "250,5%", End::Enter(Modifiers::NONE)); + assert_eq!(size(&app), (251.0, 50.0), "{name}, 250,5%"); + } +} + /// A length field of the New Document dialog keeps the focus on Ctrl/Cmd+Enter, and the dialog /// creates no document. #[test] diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 4c5891c1a..ed4fb0ac1 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -460,7 +460,8 @@ mod decimal_point_tests { let got = mm(text).unwrap_or(f64::NAN); assert!((got - want).abs() < 1e-9, "{text} is a decimal: {got}"); } - assert_eq!(share_of(&decimal_point("12,5%"), 200.0), Some(25.0)); + assert_eq!(share_of("12,5%", 200.0), Some(25.0)); + assert_eq!(share_of("1,000%", 200.0), None, "1,000% reads as thousands: refused, not 1%"); } } diff --git a/crates/ui-egui/src/widgets/numeric.rs b/crates/ui-egui/src/widgets/numeric.rs index a1a0433ae..7f619aa23 100644 --- a/crates/ui-egui/src/widgets/numeric.rs +++ b/crates/ui-egui/src/widgets/numeric.rs @@ -116,7 +116,7 @@ fn unit_field( let scrubbed = scrub::field(ui, id, rect, &buf, value.map(|v| unit.from_pt(v)), STEP_DECIMALS, &bounds_unit); let commit = commit && buf != shown; if commit { - match read(&buf).or_else(|| if share_of_value { share_of(&decimal_point(&buf), value?) } else { None }) { + match read(&buf).or_else(|| if share_of_value { share_of(&buf, value?) } else { None }) { Some(v) => { let clamped = within(v, &bounds); let formatted = unit.format(clamped); @@ -152,9 +152,11 @@ fn unit_field( } /// A percentage typed alone (`50%`, `150 %`, `100/3%`) as that share of `value`; `None` for any -/// other text. `Unit::parse` reads a bare percentage as nothing, having no value to take it of. +/// other text. `Unit::parse` reads a bare percentage as nothing, having no value to take it of. A +/// decimal comma is read ([`decimal_point`]) after the `%` is dropped, so `12,5%` is 12.5% and +/// `1,000%` is refused as thousands rather than read as 1%. pub(super) fn share_of(text: &str, value: f64) -> Option { - let percent = vectorcraft_doc::parse_number(text.trim().strip_suffix('%')?)?; + let percent = vectorcraft_doc::parse_number(&decimal_point(text.trim().strip_suffix('%')?))?; Some(value * percent / 100.0) }