Plug-ins: whole-number fields keep values past 2^53 exactly as typed

This commit is contained in:
webdevtodayjason 2026-10-12 01:33:29 +00:00 committed by bflatastic
parent 2b7d5f964f
commit 31ae1eba0b
2 changed files with 126 additions and 2 deletions

View File

@ -322,8 +322,19 @@ pub(super) fn schema_fields(ui: &mut egui::Ui, d: &mut Dialog, specs: &[(String,
crate::widgets::range_field(ui, ("plugin-num", k), x, *min..=*max, "", 2, 100.0).map(|x| json!(x))
}
ParamSpec::Int { min, max, .. } => {
let x = cur.as_i64().unwrap_or(*min) as f64;
crate::widgets::range_field(ui, ("plugin-int", k), x, *min as f64..=*max as f64, "", 0, 100.0).map(|x| json!(x as i64))
let x = cur.as_i64().unwrap_or(*min).clamp(*min, *max);
// An f64 holds every whole number up to 2^53, so the numeric field (arrows, wheel, arithmetic) is exact there.
// Past it, the number typed is read as an integer.
const EXACT: i64 = 1 << 53;
if -EXACT <= *min && *max <= EXACT {
crate::widgets::range_field(ui, ("plugin-int", k), x as f64, *min as f64..=*max as f64, "", 0, 100.0).map(|x| json!(x as i64))
} else {
let shown = x.to_string();
crate::widgets::take_dialog_focus(ui, ui.id().with(("plugin-int", k)), &shown);
crate::widgets::text_field(ui, ("plugin-int", k), Some(&shown), 100.0, 1)
.and_then(|text| text.parse::<i64>().ok())
.map(|x| json!(x.clamp(*min, *max)))
}
}
ParamSpec::Bool { .. } => {
let b = cur.as_bool().unwrap_or(false);

View File

@ -392,6 +392,119 @@ fn given_headings_are_shown_as_they_are() {
assert_eq!((spec(&d.kind).heading)(&d), "Black");
}
#[test]
fn plugin_integer_field_preserves_precision_and_clamps_exactly() {
use egui::{Event, Key, Modifiers, PointerButton};
use vectorcraft_plugins::ParamSpec;
let ctx = egui::Context::default();
theme::install_fonts(&ctx);
theme::apply(&ctx, Default::default());
let min = -9007199254740995_i64;
let max = 9007199254740995_i64;
let specs = vec![("seed".into(), ParamSpec::Int { min, max, default: 0 })];
let mut d = Dialog::new(plugin::KIND, json!({"seed": 0}));
let mut frame = |events: Vec<Event>| {
let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| {
form::schema_fields(ui, &mut d, &specs);
});
out.textures_delta.clear();
(out, d.fields["seed"].clone())
};
fn number_rect(shape: &egui::Shape) -> Option<egui::Rect> {
match shape {
egui::Shape::Text(t) if t.galley.text() == "0" => Some(t.visual_bounding_rect()),
egui::Shape::Vec(shapes) => shapes.iter().find_map(number_rect),
_ => None,
}
}
let (out, _) = frame(vec![]);
let pos = out.shapes.iter().find_map(|s| number_rect(&s.shape)).unwrap().center();
for (text, expected) in [
("9007199254740993", 9007199254740993_i64),
("9007199254740996", max),
("-9007199254740996", min),
("invalid", min),
("1.5", min),
("9223372036854775808", min),
] {
frame(vec![
Event::PointerMoved(pos),
Event::PointerButton { pos, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::default() },
]);
frame(vec![Event::PointerButton { pos, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::default() }]);
frame(vec![
Event::Key {
key: Key::A,
physical_key: None,
pressed: true,
repeat: false,
modifiers: Modifiers { ctrl: true, command: true, ..Default::default() },
},
Event::Text(text.into()),
]);
let (_, value) =
frame(vec![Event::Key { key: Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::default() }]);
assert_eq!(value, json!(expected), "{text}");
}
}
#[test]
fn plugin_integer_field_with_small_bounds_keeps_numeric_editing() {
use egui::{Event, Key, Modifiers, PointerButton};
use vectorcraft_plugins::ParamSpec;
let ctx = egui::Context::default();
theme::install_fonts(&ctx);
theme::apply(&ctx, Default::default());
let min = 1;
let max = 16;
let specs = vec![("seed".into(), ParamSpec::Int { min, max, default: 0 })];
let mut d = Dialog::new(plugin::KIND, json!({"seed": 3}));
let mut frame = |events: Vec<Event>| {
let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| {
form::schema_fields(ui, &mut d, &specs);
});
out.textures_delta.clear();
(out, d.fields["seed"].clone())
};
fn number_rect(shape: &egui::Shape) -> Option<egui::Rect> {
match shape {
egui::Shape::Text(t) if t.galley.text() == "3" => Some(t.visual_bounding_rect()),
egui::Shape::Vec(shapes) => shapes.iter().find_map(number_rect),
_ => None,
}
}
let (out, _) = frame(vec![]);
let pos = out.shapes.iter().find_map(|s| number_rect(&s.shape)).unwrap().center();
frame(vec![
Event::PointerMoved(pos),
Event::PointerButton { pos, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::default() },
]);
frame(vec![Event::PointerButton { pos, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::default() }]);
frame(vec![Event::Key { key: Key::ArrowUp, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::default() }]);
let (_, value) = frame(vec![Event::Key { key: Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::default() }]);
assert_eq!(value, json!(4));
for (text, expected) in [("7", 7), ("2 + 3", 5), ("17", max)] {
frame(vec![
Event::PointerMoved(pos),
Event::PointerButton { pos, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::default() },
]);
frame(vec![Event::PointerButton { pos, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::default() }]);
frame(vec![
Event::Key {
key: Key::A,
physical_key: None,
pressed: true,
repeat: false,
modifiers: Modifiers { ctrl: true, command: true, ..Default::default() },
},
Event::Text(text.into()),
]);
let (_, value) =
frame(vec![Event::Key { key: Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::default() }]);
assert_eq!(value, json!(expected), "{text}");
}
}
#[test]
fn a_dialog_opens_with_its_first_field_focused_so_typing_and_enter_apply() {
let mut app = app();