pub type Converter = Box<dyn Fn(f64, f64) -> String>;
pub struct PositionInfo {
converters: Vec<(String, Converter)>,
}
impl Default for PositionInfo {
fn default() -> Self {
Self::with_xy()
}
}
impl PositionInfo {
pub fn new(converters: Vec<(String, Converter)>) -> Self {
Self { converters }
}
pub fn with_xy() -> Self {
Self::new(vec![
("X".to_owned(), Box::new(|x: f64, _y: f64| format_value(x))),
("Y".to_owned(), Box::new(|_x: f64, y: f64| format_value(y))),
])
}
pub fn polar() -> Self {
Self::new(vec![
(
"Radius".to_owned(),
Box::new(|x: f64, y: f64| format_value(x.hypot(y))),
),
(
"Angle".to_owned(),
Box::new(|x: f64, y: f64| format_value(y.atan2(x).to_degrees())),
),
])
}
pub fn push(&mut self, label: impl Into<String>, converter: Converter) {
self.converters.push((label.into(), converter));
}
pub fn push_numeric(
&mut self,
label: impl Into<String>,
converter: impl Fn(f64, f64) -> f64 + 'static,
) {
self.converters.push((
label.into(),
Box::new(move |x, y| format_value(converter(x, y))),
));
}
pub fn len(&self) -> usize {
self.converters.len()
}
pub fn is_empty(&self) -> bool {
self.converters.is_empty()
}
pub fn values(&self, cursor: Option<[f64; 2]>) -> Vec<String> {
match cursor {
None => vec![EMPTY_PLACEHOLDER.to_owned(); self.converters.len()],
Some([x, y]) => self
.converters
.iter()
.map(|(_label, func)| func(x, y))
.collect(),
}
}
pub fn ui(&self, ui: &mut egui::Ui, cursor: Option<[f64; 2]>) {
ui.horizontal(|ui| {
let values = self.values(cursor);
for ((label, _func), value) in self.converters.iter().zip(values) {
ui.strong(format!("{label}:"));
ui.label(value);
ui.add_space(8.0);
}
});
}
}
const EMPTY_PLACEHOLDER: &str = "------";
pub fn format_value(value: f64) -> String {
if value.is_nan() {
return "nan".to_owned();
}
if value.is_infinite() {
return if value < 0.0 {
"-inf".to_owned()
} else {
"inf".to_owned()
};
}
crate::widget::stats_widget::format_significant(value, 7)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_xy() {
let p = PositionInfo::default();
assert_eq!(p.len(), 2);
let v = p.values(Some([3.0, 4.0]));
assert_eq!(v, vec!["3".to_owned(), "4".to_owned()]);
}
#[test]
fn no_cursor_shows_placeholder() {
let p = PositionInfo::default();
let v = p.values(None);
assert_eq!(v, vec!["------".to_owned(), "------".to_owned()]);
}
#[test]
fn polar_radius_is_hypot() {
let p = PositionInfo::polar();
let v = p.values(Some([3.0, 4.0]));
assert_eq!(v[0], "5");
assert_eq!(v[1], "53.1301");
}
#[test]
fn polar_angle_on_axes() {
let p = PositionInfo::polar();
let v = p.values(Some([1.0, 0.0]));
assert_eq!(v[0], "1");
assert_eq!(v[1], "0");
let v = p.values(Some([0.0, 1.0]));
assert_eq!(v[1], "90");
}
#[test]
fn custom_numeric_converter() {
let mut p = PositionInfo::new(vec![]);
p.push_numeric("Sum", |x, y| x + y);
assert!(!p.is_empty());
let v = p.values(Some([2.5, 1.5]));
assert_eq!(v, vec!["4".to_owned()]);
}
#[test]
fn custom_string_converter() {
let p = PositionInfo::new(vec![(
"Quad".to_owned(),
Box::new(|x: f64, y: f64| {
if x >= 0.0 && y >= 0.0 {
"I".to_owned()
} else {
"other".to_owned()
}
}),
)]);
assert_eq!(p.values(Some([1.0, 1.0])), vec!["I".to_owned()]);
assert_eq!(p.values(Some([-1.0, 1.0])), vec!["other".to_owned()]);
}
#[test]
fn format_value_seven_sig_figs() {
assert_eq!(format_value(1.0 / 3.0), "0.3333333");
assert_eq!(format_value(1234567.0), "1234567");
assert_eq!(format_value(12345678.0), "1.234568e+07");
}
#[test]
fn format_value_non_finite() {
assert_eq!(format_value(f64::NAN), "nan");
assert_eq!(format_value(f64::INFINITY), "inf");
assert_eq!(format_value(f64::NEG_INFINITY), "-inf");
}
}