mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-27 20:26:48 -06:00
UI parity: form dialogs open ready to type
Column Width, Row Height and the other form dialogs now open with the cursor in their first field and its value selected, as in Excel: Alt+H, O, W, then 12 and Enter sets the width without clicking into the field first. - The first text or number field takes focus once it can (a new window's first frame is a sizing pass that ignores focus requests), and its value is selected so typing replaces it. - Number fields commit while typing, so Enter or OK in the same frame no longer confirms the old value. Tests: tests/dialog_focus.rs (Column Width and Row Height: type, Enter, value applied). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
84acf6293b
commit
efd081acc0
@@ -36,6 +36,9 @@ pub struct Dialog {
|
||||
pub result: Option<Json>,
|
||||
/// "Show password" is ticked: password fields show their text.
|
||||
pub show_password: bool,
|
||||
/// The first text or number field still has to take keyboard focus (set when a form opens,
|
||||
/// used once): like Excel, Column Width opens ready to type, its value selected.
|
||||
focus_first_field: bool,
|
||||
picture_inbox: Option<crate::Inbox>,
|
||||
picture_target: Option<(u64, usize)>,
|
||||
/// The dialog this one was opened from (Name Manager › New…): it comes back when this closes.
|
||||
@@ -70,6 +73,7 @@ impl Dialog {
|
||||
list_index: 0,
|
||||
result: None,
|
||||
show_password: false,
|
||||
focus_first_field: true,
|
||||
picture_inbox: None,
|
||||
picture_target: None,
|
||||
back: None,
|
||||
@@ -88,6 +92,7 @@ impl Dialog {
|
||||
list_index: 0,
|
||||
result: None,
|
||||
show_password: false,
|
||||
focus_first_field: false,
|
||||
picture_inbox: None,
|
||||
picture_target: None,
|
||||
back: None,
|
||||
@@ -1387,6 +1392,28 @@ fn form(ui: &mut egui::Ui, d: &mut Dialog, confirm: &mut bool, open: &mut bool)
|
||||
ok_cancel(ui, confirm, open);
|
||||
}
|
||||
|
||||
/// Gives keyboard focus to the first text or number field the first time a form is drawn. A
|
||||
/// focused number field is in typing mode with its value selected, so Alt+H, O, W then 12 and
|
||||
/// Enter sets the width, as in Excel.
|
||||
/// Keeps asking until the field really has focus: a new window's first frame is an invisible
|
||||
/// sizing pass where a focus request is ignored.
|
||||
fn take_first_focus(d: &mut Dialog, r: &egui::Response) {
|
||||
if !d.focus_first_field {
|
||||
return;
|
||||
}
|
||||
if !r.has_focus() {
|
||||
r.request_focus();
|
||||
return;
|
||||
}
|
||||
// Focus has landed, and the field is now a text box: select its whole value so typing
|
||||
// replaces it (the field's own select-on-focus fired before it was a text box).
|
||||
let mut state = egui::text_edit::TextEditState::load(&r.ctx, r.id).unwrap_or_default();
|
||||
let all = egui::text::CCursorRange::two(egui::text::CCursor::new(0), egui::text::CCursor::new(usize::MAX / 2));
|
||||
state.cursor.set_char_range(Some(all));
|
||||
state.store(&r.ctx, r.id);
|
||||
d.focus_first_field = false;
|
||||
}
|
||||
|
||||
/// Draws a dialog's `fields`, editing its values.
|
||||
fn form_fields(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
egui::Grid::new("form").num_columns(1).spacing(vec2(8.0, 6.0)).show(ui, |ui| {
|
||||
@@ -1406,6 +1433,7 @@ fn form_fields(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
} else {
|
||||
ui.add(egui::TextEdit::singleline(&mut s).desired_width(320.0))
|
||||
};
|
||||
take_first_focus(d, &r);
|
||||
if r.changed() {
|
||||
d.values.insert(key.into(), json!(s));
|
||||
}
|
||||
@@ -1426,7 +1454,11 @@ fn form_fields(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(label);
|
||||
let mut n = d.values.get(key).and_then(Json::as_f64).unwrap_or(0.0);
|
||||
if ui.add(egui::DragValue::new(&mut n).speed(0.5)).changed() {
|
||||
// Typed values count straight away: otherwise Enter or OK in the same frame
|
||||
// confirms the old value, because a number field only commits when it loses focus.
|
||||
let r = ui.add(egui::DragValue::new(&mut n).speed(0.5).update_while_editing(true));
|
||||
take_first_focus(d, &r);
|
||||
if r.changed() {
|
||||
d.values.insert(key.into(), json!(n));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
//! Form dialogs open ready to type, as in Excel: Alt+H, O, W then a number and Enter sets the
|
||||
//! column width without clicking into the field first.
|
||||
|
||||
use egui::{Event, Key, vec2};
|
||||
use gridcraft_engine::Session;
|
||||
use gridcraft_engine::core::CellRef;
|
||||
use gridcraft_ui_egui::SheetApp;
|
||||
use serde_json::json;
|
||||
|
||||
fn harness() -> egui_kittest::Harness<'static, SheetApp> {
|
||||
let mut session = Session::new();
|
||||
session.new_workbook();
|
||||
session.execute("selection.set", json!({"cell": "C3"})).unwrap();
|
||||
let mut h = egui_kittest::Harness::builder().with_size(vec2(1200.0, 800.0)).build_ui_state(
|
||||
|ui, app: &mut SheetApp| {
|
||||
app.logic(&ui.ctx().clone());
|
||||
app.ui(ui);
|
||||
},
|
||||
SheetApp::new(session, Default::default()),
|
||||
);
|
||||
h.run_steps(6);
|
||||
h
|
||||
}
|
||||
|
||||
fn type_and_enter(h: &mut egui_kittest::Harness<'static, SheetApp>, text: &str) {
|
||||
for c in text.chars() {
|
||||
h.input_mut().events.push(Event::Text(c.to_string()));
|
||||
h.run_steps(1);
|
||||
}
|
||||
h.key_press(Key::Enter);
|
||||
h.run_steps(6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_width_opens_with_the_width_ready_to_type() {
|
||||
let mut h = harness();
|
||||
h.state_mut().open_dialog("columnWidth", json!({}));
|
||||
h.run_steps(6);
|
||||
type_and_enter(&mut h, "12");
|
||||
assert!(h.state().dialog.is_none(), "Enter confirmed the dialog");
|
||||
let chars = {
|
||||
let d = h.state().session.active().unwrap();
|
||||
let sheet = d.wb.active().unwrap();
|
||||
gridcraft_engine::cmd::format::points_to_chars(sheet.col_width(CellRef::parse("C3").unwrap().col) as f64)
|
||||
};
|
||||
assert!((chars - 12.0).abs() < 0.2, "column C is 12 characters wide, got {chars}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_height_opens_with_the_height_ready_to_type() {
|
||||
let mut h = harness();
|
||||
h.state_mut().open_dialog("rowHeight", json!({}));
|
||||
h.run_steps(6);
|
||||
type_and_enter(&mut h, "40");
|
||||
assert!(h.state().dialog.is_none(), "Enter confirmed the dialog");
|
||||
let height = {
|
||||
let d = h.state().session.active().unwrap();
|
||||
d.wb.active().unwrap().row_height(CellRef::parse("C3").unwrap().row)
|
||||
};
|
||||
assert!((height as f64 - 40.0).abs() < 0.6, "row 3 is 40 high, got {height}");
|
||||
}
|
||||
Reference in New Issue
Block a user