Files
gridcraft/crates/ui-egui/tests/keyboard_shortcuts.rs
T
Brandon Thomas b2101947ca Merge branch 'land' into fix-388
# Conflicts:
#	docs/ui-parity.md
2026-10-11 16:01:53 -04:00

152 lines
7.3 KiB
Rust

//! Excel keyboard shortcuts (crate::keymap) dispatched through real egui key events.
use egui::{Event, Key, Modifiers};
use gridcraft_engine::Session;
use gridcraft_engine::core::CellRef;
use gridcraft_engine::model::BorderStyle;
use gridcraft_ui_egui::SheetApp;
use gridcraft_ui_egui::keymap::{Shortcut, lookup};
use serde_json::json;
type Harness = egui_kittest::Harness<'static, SheetApp>;
fn harness(session: Session) -> Harness {
let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state(
|ui, app: &mut SheetApp| {
let ctx = ui.ctx().clone();
app.logic(&ctx);
app.ui(ui);
},
SheetApp::new(session, Default::default()),
);
h.run_steps(4);
h
}
fn book(selected: &str) -> Session {
let mut s = Session::new();
s.new_workbook();
s.execute("range.setValues", json!({"range": "A1", "values": [[1.5, 2], [3, 4], [5, 6]]})).unwrap();
s.execute("cell.set", json!({"cell": "C1", "input": "=A1*2"})).unwrap();
s.execute("selection.set", json!({"range": selected})).unwrap();
s
}
/// A key press as a US keyboard reports it (the physical key is the unshifted one).
fn key(h: &mut Harness, k: Key, m: Modifiers) {
h.input_mut().events.push(Event::Key { key: k, physical_key: Some(k), pressed: true, repeat: false, modifiers: m });
h.input_mut().events.push(Event::Key { key: k, physical_key: Some(k), pressed: false, repeat: false, modifiers: m });
h.run_steps(2);
}
fn style(h: &Harness, a1: &str) -> gridcraft_engine::model::Style {
let d = h.state().session.active().unwrap();
let sh = d.wb.active().unwrap();
d.wb.styles.get(sh.style_id(CellRef::parse(a1).unwrap())).clone()
}
/// Ctrl on Windows and Linux sets both `ctrl` and `command`.
const CTRL: Modifiers = Modifiers { alt: false, ctrl: true, shift: false, mac_cmd: false, command: true };
const CTRL_SHIFT: Modifiers = Modifiers { shift: true, ..CTRL };
#[test]
fn ctrl_shift_symbols_apply_number_formats_and_borders() {
let mut h = harness(book("A1:B2"));
key(&mut h, Key::Num4, CTRL_SHIFT);
assert_eq!(style(&h, "B2").num_fmt.as_str(), "$#,##0.00_);($#,##0.00)");
key(&mut h, Key::Num5, CTRL_SHIFT);
assert_eq!(style(&h, "A1").num_fmt.as_str(), "0%");
key(&mut h, Key::Backtick, CTRL_SHIFT);
assert_eq!(style(&h, "A1").num_fmt.as_str(), "General");
key(&mut h, Key::Num7, CTRL_SHIFT);
assert!(style(&h, "A1").borders.top.style != BorderStyle::None && style(&h, "B2").borders.right.style != BorderStyle::None);
key(&mut h, Key::Minus, CTRL_SHIFT);
assert!(style(&h, "A1").borders.top.style == BorderStyle::None);
// Ctrl+Shift+End extends from the anchor to the last used cell.
key(&mut h, Key::End, CTRL_SHIFT);
assert_eq!(h.state().session.active().unwrap().selection.current().a1(), "A1:C3");
}
#[test]
fn copy_formula_from_above_and_function_keys() {
let mut h = harness(book("C2"));
key(&mut h, Key::Quote, CTRL);
assert_eq!(h.state().editor.as_ref().map(|e| e.text.clone()).as_deref(), Some("=A1*2"));
key(&mut h, Key::Escape, Modifiers::NONE);
assert!(h.state().editor.is_none());
let sheets = h.state().session.active().unwrap().wb.sheets.len();
key(&mut h, Key::F11, Modifiers::SHIFT);
assert_eq!(h.state().session.active().unwrap().wb.sheets.len(), sheets + 1);
// Back on the data: Alt+F1 charts the current region, Ctrl+Alt+= zooms in.
h.state_mut().session.run("sheet.previous", json!({})).unwrap();
h.state_mut().session.run("selection.set", json!({"range": "A1:B3"})).unwrap();
key(&mut h, Key::F1, Modifiers::ALT);
assert_eq!(h.state().session.active().unwrap().wb.sheets[0].charts.len(), 1);
key(&mut h, Key::Equals, Modifiers { alt: true, ..CTRL });
assert_eq!(h.state().session.active().unwrap().wb.sheets[0].zoom, 110);
}
#[test]
fn windows_and_mac_variants() {
let cmd = CTRL;
let mac_cmd = Modifiers { mac_cmd: true, command: true, ..Modifiers::NONE };
let ctrl = Modifiers::CTRL;
let run = |id: &'static str| Some(Shortcut::Run(id, json!({})));
assert_eq!(lookup(Key::L, Some(Key::L), cmd, false), run("insert.table"));
assert_eq!(lookup(Key::L, Some(Key::L), mac_cmd, true), run("home.alignLeft"));
assert_eq!(lookup(Key::E, Some(Key::E), mac_cmd, true), run("home.alignCenter"));
assert_eq!(lookup(Key::E, Some(Key::E), ctrl, true), run("edit.flashFill"));
assert_eq!(lookup(Key::Equals, Some(Key::Equals), Modifiers::ALT, false), run("formulas.autoSum"));
// The shifted character doesn't matter: Ctrl+Shift+1 is the Number format wherever `!` is.
assert_eq!(
lookup(Key::Exclamationmark, Some(Key::Num1), CTRL_SHIFT, false),
Some(Shortcut::Run("home.numberFormat", json!({"code": "#,##0.00"})))
);
// Navigation keys go by what they are, not where: the numpad's arrows and Page Down (Num
// Lock off) report Num6 / Num3 as their physical keys.
let alt_shift = Modifiers { alt: true, shift: true, ..Modifiers::NONE };
assert_eq!(lookup(Key::ArrowRight, Some(Key::Num6), alt_shift, false), run("data.group"));
assert_eq!(lookup(Key::ArrowLeft, Some(Key::Num4), alt_shift, false), run("data.ungroup"));
assert_eq!(lookup(Key::PageDown, Some(Key::Num3), Modifiers::ALT, false), Some(Shortcut::ScreenRight));
assert_eq!(lookup(Key::PageDown, Some(Key::Num3), CTRL_SHIFT, false), None);
// Plain keys stay with the grid (typing, navigation).
assert_eq!(lookup(Key::A, Some(Key::A), Modifiers::NONE, false), None);
assert_eq!(lookup(Key::F2, Some(Key::F2), Modifiers::NONE, false), None);
}
#[test]
fn space_selection_shortcuts_do_not_start_an_edit() {
let mut h = harness(book("B2"));
// Shift+Space: the key press, then the space the OS types with it.
let shift = Modifiers::SHIFT;
h.input_mut().events.push(Event::Key { key: Key::Space, physical_key: Some(Key::Space), pressed: true, repeat: false, modifiers: shift });
h.input_mut().events.push(Event::Text(" ".into()));
h.run_steps(2);
assert!(h.state().editor.is_none(), "Shift+Space started an edit");
assert_eq!(h.state().session.active().unwrap().selection.ranges[0].a1(), "2:2");
h.state_mut().session.run("selection.set", json!({"range": "B2"})).unwrap();
// Ctrl+Space selects the column, also without starting an edit.
h.input_mut().events.push(Event::Key { key: Key::Space, physical_key: Some(Key::Space), pressed: true, repeat: false, modifiers: CTRL });
h.input_mut().events.push(Event::Text(" ".into()));
h.run_steps(2);
assert!(h.state().editor.is_none());
assert_eq!(h.state().session.active().unwrap().selection.ranges[0].a1(), "B:B");
// Plain typing still edits.
h.input_mut().events.push(Event::Text("x".into()));
h.run_steps(2);
assert!(h.state().editor.is_some());
}
#[test]
fn space_shortcuts_per_platform() {
let run = |id: &'static str| Some(Shortcut::Run(id, json!({})));
let mac_cmd = Modifiers { mac_cmd: true, command: true, ..Modifiers::NONE };
let space = |m, mac| lookup(Key::Space, Some(Key::Space), m, mac);
assert_eq!(space(Modifiers::SHIFT, false), run("selection.row"));
assert_eq!(space(Modifiers::SHIFT, true), run("selection.row"));
assert_eq!(space(CTRL, false), run("selection.column"));
assert_eq!(space(Modifiers::CTRL, true), run("selection.column"));
// Cmd+Space is Spotlight on macOS.
assert_eq!(space(mac_cmd, true), None);
}