use crate::{EditorModel, MainWindow, TierTableModel, gui::show_toast};
use indicatrix_editor::loading::eval_number;
use slint::ComponentHandle;
fn read_number(label: &str, text: &str) -> Result<f64, String> {
if text.trim().is_empty() {
return Err(format!(
"{label} is empty -- type a number or a calculation."
));
}
let value = eval_number(text, None).map_err(|error| error.message(label, text))?;
if value.is_finite() {
Ok(value)
} else {
Err(format!("{label} must be a finite number."))
}
}
fn signed_teeth(teeth: f64, direction: i32) -> f64 {
if direction < 0 { -teeth } else { teeth }
}
pub(in crate::gui::editor) fn setup_tier_table_model(ui: &MainWindow) {
let model = ui.global::<TierTableModel>();
model.on_offset_selected({
let ui_weak = ui.as_weak();
move |anchor, text| {
let Some(ui) = ui_weak.upgrade() else {
return false;
};
if anchor < 0 {
show_toast(
&ui,
"Click one of the selected tiers first, then press Offset.",
"info",
);
return false;
}
match read_number("Offset", &text) {
Ok(degrees) => {
ui.global::<EditorModel>()
.invoke_nudge_angle(anchor, degrees as f32);
true
}
Err(message) => {
show_toast(&ui, &message, "error");
false
}
}
}
});
model.on_add_facet({
let ui_weak = ui.as_weak();
move |tier, text| {
let Some(ui) = ui_weak.upgrade() else {
return false;
};
match read_number("Position", &text) {
Ok(position) => {
ui.global::<EditorModel>()
.invoke_facet_add(tier, position as f32);
true
}
Err(message) => {
show_toast(&ui, &message, "error");
false
}
}
}
});
model.on_rotate_tier({
let ui_weak = ui.as_weak();
move |tier, text, direction| {
let Some(ui) = ui_weak.upgrade() else {
return false;
};
match read_number("Teeth", &text) {
Ok(teeth) => {
ui.global::<EditorModel>()
.invoke_tier_rotate_indices(tier, signed_teeth(teeth, direction) as f32);
true
}
Err(message) => {
show_toast(&ui, &message, "error");
false
}
}
}
});
model.on_any_detached(|rows| {
use slint::Model;
rows.iter().any(|row| row.is_detached)
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_plain_number_is_read_as_it_is() {
assert_eq!(read_number("Offset", "0.5"), Ok(0.5));
assert_eq!(read_number("Offset", " -2 "), Ok(-2.0));
}
#[test]
fn a_calculation_is_worked_out() {
assert_eq!(read_number("Offset", "0.5+0.25"), Ok(0.75));
assert_eq!(read_number("Position", "96/4"), Ok(24.0));
assert_eq!(read_number("Teeth", "(2 + 1) * 2"), Ok(6.0));
}
#[test]
fn a_blank_field_asks_for_a_number() {
let message = read_number("Position", " ").unwrap_err();
assert_eq!(
message,
"Position is empty -- type a number or a calculation."
);
}
#[test]
fn text_that_is_no_number_gets_the_usual_message() {
assert_eq!(
read_number("Offset", "abc"),
Err("Offset 'abc' is not a number.".to_string())
);
}
#[test]
fn a_broken_calculation_says_why() {
let message = read_number("Offset", "1/0").unwrap_err();
assert!(
message.starts_with("Offset '1/0' cannot be calculated: "),
"{message}"
);
let message = read_number("Offset", "(1+2").unwrap_err();
assert!(
message.starts_with("Offset '(1+2' cannot be calculated: "),
"{message}"
);
}
#[test]
fn infinity_and_nan_are_refused() {
assert_eq!(
read_number("Teeth", "inf"),
Err("Teeth must be a finite number.".to_string())
);
assert_eq!(
read_number("Teeth", "NaN"),
Err("Teeth must be a finite number.".to_string())
);
}
#[test]
fn the_rotate_direction_signs_the_teeth() {
assert_eq!(signed_teeth(3.0, 1), 3.0);
assert_eq!(signed_teeth(3.0, -1), -3.0);
assert_eq!(
signed_teeth(-3.0, -1),
3.0,
"the sign flips, it is not forced negative"
);
}
}