gled-ui 2.32.1

gled's egui interface, shared by the desktop app and the browser
Documentation
//! Lengths as the definition editor shows them: a definition keeps metres,
//! a person drawing a lamp thinks in millimetres.

use egui::DragValue;
use egui::emath::format_with_decimals_in_range;
use std::ops::RangeInclusive;

/// Metres one millimetre makes.
const MM: f64 = 0.001;

/// A field for a length in metres, shown and typed in millimetres. Its
/// speed, range and decimals are in metres, like the value; dragging moves it
/// by a millimetre a step.
pub(crate) fn millimetres(metres: &mut f32) -> DragValue<'_> {
    DragValue::new(metres)
        .speed(MM)
        .suffix(" mm")
        .custom_formatter(shown_in_mm)
        .custom_parser(typed_in_mm)
}

/// `metres` as millimetres, with the decimals the field asks for in metres.
fn shown_in_mm(metres: f64, decimals: RangeInclusive<usize>) -> String {
    // three decimals fewer in millimetres than in metres
    let decimals = decimals.start().saturating_sub(3)..=decimals.end().saturating_sub(3);
    format_with_decimals_in_range(metres / MM, decimals)
}

/// Metres from millimetres typed into the field, with or without the unit.
fn typed_in_mm(text: &str) -> Option<f64> {
    let text: String = text
        .chars()
        .filter(|c| !c.is_whitespace())
        .map(|c| if c == '−' { '-' } else { c })
        .collect();
    let number = text.strip_suffix("mm").unwrap_or(&text);
    number.parse::<f64>().ok().map(|mm| mm * MM)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn a_length_is_shown_in_millimetres() {
        assert_eq!(shown_in_mm(0.0167, 3..=4), "16.7");
        assert_eq!(shown_in_mm(0.25, 3..=5), "250");
        assert_eq!(shown_in_mm(-0.1, 0..=3), "-100");
    }

    #[test]
    fn a_length_is_typed_in_millimetres() {
        let typed = |text| typed_in_mm(text).map(|metres| (metres * 1e6).round() / 1e6);
        assert_eq!(typed("16.7"), Some(0.0167));
        assert_eq!(typed(" 250 mm"), Some(0.25));
        assert_eq!(typed("−5"), Some(-0.005));
        assert_eq!(typed("lots"), None);
    }
}