Merge PR #1331: Form dialogs open ready to type (Column Width, Row Height)

This commit is contained in:
aiongg
2026-10-22 02:35:14 +00:00
2 changed files with 94 additions and 1 deletions
+33 -1
View File
@@ -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));
}
});
+61
View File
@@ -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}");
}