Numeric: clamp committed values in point units, schedule rejection highlight repaint, and update roadmap/area evidence

This commit is contained in:
Taha-Mahmoodi
2026-10-30 16:55:55 +07:00
committed by bflatastic
parent 418f1bffc3
commit 5da8c8e495
6 changed files with 41 additions and 8 deletions
+27 -2
View File
@@ -2534,9 +2534,34 @@ fn color_halftone_invalid_input_does_not_revert_to_sub_minimum_value() {
frame(&mut app, vec![egui::Event::Text("0.5 px".into())]);
frame(&mut app, vec![egui::Event::Key { key: egui::Key::Tab, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }]);
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["maxRadius"], serde_json::json!(4.0));
// 2. Type invalid input "1jgghgh"
// 2. Refocus Max Radius before typing invalid text
crate::widgets::arm_field_focus(&ctx, ("dlg-len", "maxRadius"));
frame(&mut app, vec![]);
frame(&mut app, vec![egui::Event::Text("1jgghgh".into())]);
frame(&mut app, vec![egui::Event::Key { key: egui::Key::Tab, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }]);
// The value remains the valid clamped minimum 4.0 pt, never 0.5 px
// The value remains the valid clamped minimum 4.0 pt, never 0.5 px, and rejection flash is set
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["maxRadius"], serde_json::json!(4.0));
assert!(app.ui.dialog.as_ref().unwrap().fields["maxRadius"] == serde_json::json!(4.0));
}
#[test]
fn bounded_field_with_inches_does_not_clamp_point_bounds_to_inch_numbers() {
let mut app = app();
let _id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap()["id"].as_u64().unwrap();
// Switch document units to Inches
app.run("document.setup", json!({"units": "Inches"})).unwrap();
let ctx = egui::Context::default();
theme::install_fonts(&ctx);
theme::apply(&ctx, Default::default());
let frame = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| show(app, ui.ctx()));
out.textures_delta.clear();
};
app.run("select.all", json!({})).unwrap();
app.run("effect.dialog", json!({"effect": "pixelate.colorHalftone"})).unwrap();
frame(&mut app, vec![]);
// Type 1 in: 72 pt is within 4..127 pt range and must not clamp to ~1.76 pt
frame(&mut app, vec![egui::Event::Text("1 in".into())]);
frame(&mut app, vec![egui::Event::Key { key: egui::Key::Tab, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }]);
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["maxRadius"], serde_json::json!(72.0));
}
+1 -1
View File
@@ -87,7 +87,7 @@ impl<T> std::fmt::Debug for HashOnly<T> {
}
/// `parent.with(salt)` for a salt that needs only `Hash`.
fn id_with(parent: egui::Id, salt: impl std::hash::Hash) -> egui::Id {
pub(crate) fn id_with(parent: egui::Id, salt: impl std::hash::Hash) -> egui::Id {
parent.with(HashOnly(salt))
}
+6 -4
View File
@@ -102,10 +102,11 @@ fn unit_field(
// ↑/↓ and the wheel step in the field's unit; a drag on its label scrubs it.
let typed = typed_unit(ui.ctx(), unit);
let read = |b: &str| typed.parse(&decimal_point(b));
let bounds = unit.from_pt(*bounds.start())..=unit.from_pt(*bounds.end());
let bounds_unit = unit.from_pt(*bounds.start())..=unit.from_pt(*bounds.end());
let stops: Vec<f64> = stops.iter().map(|&s| unit.from_pt(s)).collect();
let stepped = step_value(ui, &resp, &mut buf, &bounds, (|b: &str| Some(unit.from_pt(read(b)?)), |v| unit.format(unit.to_pt(v))), None, &stops);
let scrubbed = scrub::field(ui, id, rect, &buf, value.map(|v| unit.from_pt(v)), STEP_DECIMALS, &bounds);
let stepped =
step_value(ui, &resp, &mut buf, &bounds_unit, (|b: &str| Some(unit.from_pt(read(b)?)), |v| unit.format(unit.to_pt(v))), None, &stops);
let scrubbed = scrub::field(ui, id, rect, &buf, value.map(|v| unit.from_pt(v)), STEP_DECIMALS, &bounds_unit);
let commit = commit && buf != shown;
if commit {
match read(&buf).or_else(|| if share_of_value { share_of(&decimal_point(&buf), value?) } else { None }) {
@@ -117,7 +118,8 @@ fn unit_field(
}
None => {
ui.data_mut(|d| d.remove::<String>(id));
ui.data_mut(|d| d.insert_temp(id.with("invalid-flash"), true));
ui.data_mut(|d| d.insert_temp(id.with("invalid-flash"), ui.input(|i| i.time + 0.25)));
ui.ctx().request_repaint();
None
}
}
+5 -1
View File
@@ -237,7 +237,11 @@ pub(super) fn recessed_text(
.allocate_ui_with_layout(vec2(width, height), layout, |ui| {
ui.set_min_size(vec2(width, height));
let rect = ui.max_rect();
let invalid = ui.data_mut(|d| d.remove_temp::<bool>(id.with("invalid-flash"))).unwrap_or(false);
let now = ui.input(|i| i.time);
let invalid = ui.data(|d| d.get_temp::<f64>(id.with("invalid-flash"))).is_some_and(|end| now <= end);
if invalid {
ui.ctx().request_repaint();
}
let stroke = if invalid {
Stroke::new(1.5, t.accent_strong)
} else if editing {