Merge pull request #1327 from webdevtodayjason/fix/plugin-dialog-int
Plug-ins: whole-number fields keep values past 2^53 exactly as typed
This commit is contained in:
commit
2c5262c5dd
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -392,6 +392,89 @@ fn given_headings_are_shown_as_they_are() {
|
|||
assert_eq!((spec(&d.kind).heading)(&d), "Black");
|
||||
}
|
||||
|
||||
/// A plug-in dialog with one whole-number field `seed` in `min..=max`, driven by egui events.
|
||||
struct PluginIntField {
|
||||
ctx: egui::Context,
|
||||
d: Dialog,
|
||||
specs: Vec<(String, vectorcraft_plugins::ParamSpec)>,
|
||||
pos: egui::Pos2,
|
||||
}
|
||||
|
||||
impl PluginIntField {
|
||||
fn new(min: i64, max: i64, start: i64) -> Self {
|
||||
let ctx = egui::Context::default();
|
||||
theme::install_fonts(&ctx);
|
||||
theme::apply(&ctx, Default::default());
|
||||
let specs = vec![("seed".into(), vectorcraft_plugins::ParamSpec::Int { min, max, default: 0 })];
|
||||
let mut f = PluginIntField { ctx, d: Dialog::new(plugin::KIND, json!({ "seed": start })), specs, pos: egui::Pos2::ZERO };
|
||||
fn number_rect(shape: &egui::Shape, text: &str) -> Option<egui::Rect> {
|
||||
match shape {
|
||||
egui::Shape::Text(t) if t.galley.text() == text => Some(t.visual_bounding_rect()),
|
||||
egui::Shape::Vec(shapes) => shapes.iter().find_map(|s| number_rect(s, text)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
let out = f.run(vec![]);
|
||||
f.pos = out.shapes.iter().find_map(|s| number_rect(&s.shape, &start.to_string())).unwrap().center();
|
||||
f
|
||||
}
|
||||
|
||||
fn run(&mut self, events: Vec<egui::Event>) -> egui::FullOutput {
|
||||
let (d, specs) = (&mut self.d, &self.specs);
|
||||
let mut out = self.ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| {
|
||||
form::schema_fields(ui, d, specs);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
out
|
||||
}
|
||||
|
||||
/// Clicks the field, sends `events`, presses Enter and returns the field's value.
|
||||
fn commit(&mut self, events: Vec<egui::Event>) -> serde_json::Value {
|
||||
use egui::{Event, Key, Modifiers, PointerButton};
|
||||
let (pos, button) = (self.pos, PointerButton::Primary);
|
||||
self.run(vec![Event::PointerMoved(pos), Event::PointerButton { pos, button, pressed: true, modifiers: Modifiers::default() }]);
|
||||
self.run(vec![Event::PointerButton { pos, button, pressed: false, modifiers: Modifiers::default() }]);
|
||||
self.run(events);
|
||||
self.run(vec![Event::Key { key: Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::default() }]);
|
||||
self.d.fields["seed"].clone()
|
||||
}
|
||||
|
||||
/// Replaces the field's text with `text` and commits it.
|
||||
fn type_text(&mut self, text: &str) -> serde_json::Value {
|
||||
let all = egui::Modifiers { ctrl: true, command: true, ..Default::default() };
|
||||
self.commit(vec![
|
||||
egui::Event::Key { key: egui::Key::A, physical_key: None, pressed: true, repeat: false, modifiers: all },
|
||||
egui::Event::Text(text.into()),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_integer_field_preserves_precision_and_clamps_exactly() {
|
||||
let (min, max) = (-9007199254740995_i64, 9007199254740995_i64);
|
||||
let mut f = PluginIntField::new(min, max, 0);
|
||||
for (text, expected) in [
|
||||
("9007199254740993", 9007199254740993_i64),
|
||||
("9007199254740996", max),
|
||||
("-9007199254740996", min),
|
||||
("invalid", min),
|
||||
("1.5", min),
|
||||
("9223372036854775808", min),
|
||||
] {
|
||||
assert_eq!(f.type_text(text), json!(expected), "{text}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_integer_field_with_small_bounds_keeps_numeric_editing() {
|
||||
let mut f = PluginIntField::new(1, 16, 3);
|
||||
let up = egui::Event::Key { key: egui::Key::ArrowUp, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() };
|
||||
assert_eq!(f.commit(vec![up]), json!(4));
|
||||
for (text, expected) in [("7", 7), ("2 + 3", 5), ("17", 16)] {
|
||||
assert_eq!(f.type_text(text), json!(expected), "{text}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dialog_opens_with_its_first_field_focused_so_typing_and_enter_apply() {
|
||||
let mut app = app();
|
||||
|
|
|
|||
Loading…
Reference in New Issue