diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 2e139d013..c74a78a0f 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -57,7 +57,7 @@ pub use numeric::{ spin_field_auto, spin_field_auto_named, spin_field_named, spin_field_stops_named, spin_kerning, spin_plain, spin_plain_named, }; #[cfg(test)] -use numeric::{decimal_point, share_of, spinner_step, step_through, step_to_multiple, within_a_millionth}; +use numeric::{decimal_point, field_number, share_of, spinner_step, step_through, step_to_multiple, within_a_millionth}; pub use panel_chrome::{bottom_bar, layout_button, list_box, orientation_button, tab_bar}; pub(crate) use text::{ arm_field_focus, dialog_focus_flag, dialog_reject_commit_flag, disarm_field_focus, enter_modifiers, focus_field, own_field_shift_enter, @@ -464,6 +464,30 @@ mod decimal_point_tests { } } +#[cfg(test)] +mod field_number_tests { + use super::*; + + /// Kerning drops one pair of parentheses around its whole text, so its automatic value `(12)` + /// reads as 12. Parentheses inside the text are refused rather than dropped, which would read + /// `(10+5)*2` as `10+5*2`, and fields without the Kerning flag refuse parentheses too. + #[test] + fn only_kerning_drops_one_pair_of_parentheses_around_the_text() { + assert_eq!(field_number("(12)", "", true), Some(12.0)); + assert_eq!(field_number(" (-25) ", "", true), Some(-25.0)); + assert_eq!(field_number("(10+5)", "", true), Some(15.0)); + assert_eq!(field_number("12", "", true), Some(12.0)); + for text in ["(10+5)*2", "2*(10+5)", "(10)+(5)", "((12))", "(12", "12)"] { + assert_eq!(field_number(text, "", true), None, "{text}"); + } + for text in ["(12)", "(10+5)*2"] { + assert_eq!(field_number(text, "", false), None, "{text} without the Kerning flag"); + } + assert_eq!(field_number("45*2°", "°", false), Some(90.0)); + assert_eq!(field_number("50% / 2", "%", false), Some(25.0)); + } +} + #[cfg(test)] mod menu_room_tests { use super::*; diff --git a/crates/ui-egui/src/widgets/numeric.rs b/crates/ui-egui/src/widgets/numeric.rs index 91cd18eeb..56ace75b8 100644 --- a/crates/ui-egui/src/widgets/numeric.rs +++ b/crates/ui-egui/src/widgets/numeric.rs @@ -454,13 +454,7 @@ fn bounded_field( let armed = take_armed_focus(ui, &id); let id = id_with(ui.scope_id(), id); let show = |value: f64| field_text(value, suffix, decimals); - // The suffix (and %, °) may follow any operand: `45*2°`, `50% / 2`. - let read = |buf: &str| { - let suffix = suffix.trim(); - let buf = decimal_point(buf); - let bare = if suffix.is_empty() { buf.to_string() } else { buf.replace(suffix, "") }; - vectorcraft_doc::parse_number(&bare.replace(['%', '°', '(', ')'], "")) - }; + let read = |buf: &str| field_number(buf, suffix, parentheses); let shown = value.map(|v| if parentheses { format!("({})", show(v)) } else { show(v) }).unwrap_or_default(); take_dialog_focus(ui, id, &shown); if armed { @@ -491,6 +485,21 @@ fn bounded_field( } } +/// The number text typed into a [`bounded_field`] with `suffix` reads as. The suffix, `%` and `°` +/// may follow any operand: `45*2°`, `50% / 2`. With `parentheses` (Kerning, which shows its +/// automatic value as `(12)`), one pair of parentheses around the whole text is dropped first, so +/// `(12)` reads as 12. Any other parentheses are refused: `(10+5)*2` reads as nothing, not as +/// `10+5*2`. +pub(super) fn field_number(buf: &str, suffix: &str, parentheses: bool) -> Option { + let suffix = suffix.trim(); + let buf = decimal_point(buf); + let bare = if suffix.is_empty() { buf.to_string() } else { buf.replace(suffix, "") }; + let bare = bare.replace(['%', '°'], ""); + let bare = bare.trim(); + let bare = if parentheses { bare.strip_prefix('(').and_then(|b| b.strip_suffix(')')).unwrap_or(bare) } else { bare }; + vectorcraft_doc::parse_number(bare) +} + /// The text [`plain_field`] shows for `value`: at most `decimals` places, without trailing zeros, /// then `suffix`. pub(super) fn field_text(value: f64, suffix: &str, decimals: usize) -> String {