From ffc481a4e4f129154873cdefce82d6c7f5b5422c Mon Sep 17 00:00:00 2001 From: pravdomil <2387356+pravdomil@users.noreply.github.com> Date: Sun, 11 Oct 2026 23:36:15 +0200 Subject: [PATCH] UI: numeric forms open with their first number focused and selected Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/forms.rs | 40 ++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/panels/forms.rs b/crates/ui-egui/src/panels/forms.rs index f23b8103..b276cd8b 100644 --- a/crates/ui-egui/src/panels/forms.rs +++ b/crates/ui-egui/src/panels/forms.rs @@ -775,11 +775,16 @@ pub fn show_form(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { let r = match &mut fl.kind { FieldKind::Number { value, speed } => { let dec = if *speed < 0.01 { 5 } else { 3 }; - ui.add(crate::widgets::drag_value(egui::DragValue::new(value).speed(*speed).max_decimals(dec))).rect + let r = ui.add(crate::widgets::drag_value(egui::DragValue::new(value).speed(*speed).max_decimals(dec))); + // The first field takes the keyboard when the form opens. (A number edits + // as text once focused; the selection is clamped to what it shows.) + if !std::mem::replace(&mut focused, true) { + crate::widgets::focus_on_open(ui.ctx(), &r, &" ".repeat(64)); + } + r.rect } FieldKind::Text(s) => { let r = ui.add(egui::TextEdit::singleline(s).desired_width(220.0)); - // The first text field takes the keyboard when the form opens. if !std::mem::replace(&mut focused, true) { crate::widgets::focus_on_open(ui.ctx(), &r, s); } @@ -956,6 +961,37 @@ mod tests { assert_eq!(f.params(), json!({"prop": "position", "value": [10.0, 20.0], "mode": "b"})); } + /// A numeric form (Layer ▸ Transform ▸ Position…) opens with its first number being edited + /// and selected: typing replaces it, and one Enter confirms. + #[test] + fn a_numeric_form_opens_with_its_first_number_selected() { + let mut app = EffectcraftApp::new(effectcraft_engine::Session::default()); + let ctx = egui::Context::default(); + crate::theme::install(&ctx, &app.tokens, crate::i18n::language(&app)); + app.dialog_state.form = Form { + title: "Position".into(), + command: "layer.setTransform".into(), + base: json!({"prop": "position"}), + fields: vec![Field::num("value[0]", "X", 960.0), Field::num("value[1]", "Y", 540.0)], + }; + app.dialog = Some(Dialog::Form); + let frame = |app: &mut EffectcraftApp, events: Vec| { + let raw = egui::RawInput { events, ..Default::default() }; + ctx.run_ui(raw, |ui| { + app.auto.begin_frame(); + crate::panels::dialogs::show(app, ui.ctx()); + }) + .textures_delta + .clear(); + }; + frame(&mut app, vec![]); + frame(&mut app, vec![]); + frame(&mut app, vec![egui::Event::Text("42".into())]); + assert_eq!(app.dialog_state.form.params()["value"], json!([42.0, 540.0])); + frame(&mut app, vec![egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }]); + assert!(app.dialog.is_none(), "one Enter confirms"); + } + /// File ▸ Export ▸ Adobe Premiere Pro Project… opens a form that says it writes Final Cut Pro /// XML; its fields register automation ids and OK runs `file.exportTimeline`. #[test]