diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 6f307d43a..eb78e4392 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -656,7 +656,8 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { } }); }); - if spec.ok.is_some() && ok_enabled && !after_panel && ctx.input(|i| i.key_pressed(egui::Key::Enter)) { + let rejected = ctx.data_mut(|d| d.remove_temp::(widgets::dialog_reject_commit_flag())).unwrap_or(false); + if spec.ok.is_some() && ok_enabled && !after_panel && !rejected && ctx.input(|i| i.key_pressed(egui::Key::Enter)) { ok = true; } if discard { diff --git a/crates/ui-egui/src/dialogs/tests.rs b/crates/ui-egui/src/dialogs/tests.rs index 6add459f4..ccc7b3dc0 100644 --- a/crates/ui-egui/src/dialogs/tests.rs +++ b/crates/ui-egui/src/dialogs/tests.rs @@ -2534,14 +2534,15 @@ 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. Refocus Max Radius before typing invalid text - crate::widgets::arm_field_focus(&ctx, ("dlg-len", "maxRadius")); + // 2. Typing invalid text on Enter must NOT confirm/close the dialog; the dialog stays open + app.run("effect.dialog", json!({"effect": "pixelate.colorHalftone"})).unwrap(); 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, 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)); + frame( + &mut app, + vec![egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }], + ); + assert!(app.ui.dialog.is_some(), "dialog must remain open on invalid Enter commit"); } #[test] diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 8590a2667..87fa6ac84 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -57,7 +57,8 @@ pub use numeric::{ use numeric::{decimal_point, share_of, spinner_step, step_through, step_to_multiple, within_a_millionth}; pub use panel_chrome::{bottom_bar, layout_button, list_box, orientation_button, tab_bar}; pub(crate) use text::{ - arm_field_focus, dialog_focus_flag, disarm_field_focus, enter_modifiers, focus_field, own_field_shift_enter, set_dialog_open, take_dialog_focus, + arm_field_focus, dialog_focus_flag, dialog_reject_commit_flag, disarm_field_focus, enter_modifiers, focus_field, own_field_shift_enter, + reject_dialog_commit, set_dialog_open, take_dialog_focus, }; pub use text::{ exact_text_field, exact_text_field_named, plain_field, plain_field_named, search_field, text_field, text_field_named, text_presets, diff --git a/crates/ui-egui/src/widgets/numeric.rs b/crates/ui-egui/src/widgets/numeric.rs index 184112790..1867eb271 100644 --- a/crates/ui-egui/src/widgets/numeric.rs +++ b/crates/ui-egui/src/widgets/numeric.rs @@ -117,8 +117,12 @@ fn unit_field( Some(clamped) } None => { - ui.data_mut(|d| d.remove::(id)); - ui.data_mut(|d| d.insert_temp(id.with("invalid-flash"), ui.input(|i| i.time + 0.25))); + let end = ui.input(|i| i.time + 0.25); + ui.data_mut(|d| { + d.remove::(id); + d.insert_temp(id.with("invalid-flash"), end); + }); + crate::widgets::reject_dialog_commit(ui.ctx()); ui.ctx().request_repaint(); None } diff --git a/crates/ui-egui/src/widgets/text.rs b/crates/ui-egui/src/widgets/text.rs index e688c0c2e..9d848f39f 100644 --- a/crates/ui-egui/src/widgets/text.rs +++ b/crates/ui-egui/src/widgets/text.rs @@ -87,6 +87,16 @@ fn dialog_open_flag() -> egui::Id { egui::Id::unique("dialog-open") } +/// The flag that tells the dialog runner a field rejected an invalid commit on Enter in this frame. +pub(crate) fn dialog_reject_commit_flag() -> egui::Id { + egui::Id::unique("dialog-reject-commit") +} + +/// Suppress confirming an open dialog on Enter during this frame when an invalid value was committed. +pub(crate) fn reject_dialog_commit(ctx: &egui::Context) { + ctx.data_mut(|d| d.insert_temp(dialog_reject_commit_flag(), true)); +} + /// The app sets this at the start of each frame: while a dialog is open, Shift+Enter in a field /// applies its value and confirms the dialog as Enter does ([`shift_enter_keeps_focus`]). pub(crate) fn set_dialog_open(ctx: &egui::Context, open: bool) { @@ -279,7 +289,8 @@ pub(super) fn recessed_text( d.insert_temp(id, buf.clone()); d.insert_temp(focus_id, focused); }); - let commit = resp.lost_focus() || kept; + let enter_commit = rows == 1 && editing && ui.input(|i| i.key_pressed(egui::Key::Enter)); + let commit = resp.lost_focus() || kept || enter_commit; (buf, commit, resp, rect) } diff --git a/docs/roadmap.md b/docs/roadmap.md index 3febbc75c..e1f58f3af 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -4,7 +4,7 @@ ## Feature submission evidence -**Feature evidence last updated:** 2026-10-22. +**Feature evidence last updated:** 2026-10-30. The shared assessment status line and revision history are current upstream; this dated section records the submission's feature evidence and revisions without rescoring upstream or claiming a new combined build or runtime measurement.