mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-29 21:26:48 -06:00
# Conflicts: # ROADMAP.md # crates/engine/src/cmd/edit.rs # crates/engine/src/cmd/formulas.rs # crates/engine/src/tests.rs # crates/formula/src/lib.rs # crates/formula/src/parser.rs # crates/formula/src/tests.rs # crates/ui-egui/src/control.rs # crates/ui-egui/src/formula_bar.rs # crates/ui-egui/src/grid.rs # crates/ui-egui/src/i18n.rs # crates/ui-egui/src/lib.rs # crates/ui-egui/src/ribbon.rs # crates/ui-egui/tests/selection.rs # docs/function-parity.md # docs/ui-parity.md
306 lines
12 KiB
Rust
306 lines
12 KiB
Rust
//! Cell selection consumes every pointer position without editing cell contents.
|
|
|
|
use egui::{Event, Modifiers, PointerButton, Pos2};
|
|
use gridcraft_engine::Session;
|
|
use gridcraft_engine::core::{CellRef, RangeRef};
|
|
use gridcraft_ui_egui::{SheetApp, grid::Geo};
|
|
use serde_json::{Value, json};
|
|
|
|
type Harness = egui_kittest::Harness<'static, SheetApp>;
|
|
|
|
fn harness(initial: &str) -> Harness {
|
|
let mut session = Session::new();
|
|
session.new_workbook();
|
|
session.execute("range.setValues", json!({"range": "A1", "values": [["sentinel", 2], [3, "untouched"]]})).unwrap();
|
|
session.execute("cell.set", json!({"cell": "C3", "input": "123"})).unwrap();
|
|
session.execute("cell.set", json!({"cell": "F7", "input": "=C3*2"})).unwrap();
|
|
session.execute("selection.set", json!({"cell": initial})).unwrap();
|
|
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 point(h: &Harness, a1: &str) -> Pos2 {
|
|
let app = h.state();
|
|
let sheet = app.session.active().unwrap().wb.active().unwrap();
|
|
Geo::new(sheet, app.grid.rect.unwrap(), app.view().scroll).cell_rect(sheet, CellRef::parse(a1).unwrap()).center()
|
|
}
|
|
|
|
fn cells(h: &Harness) -> Value {
|
|
let sheet = h.state().session.active().unwrap().wb.active().unwrap();
|
|
json!(sheet.cells.iter().map(|(pos, cell)| (pos.a1(), cell)).collect::<Vec<_>>())
|
|
}
|
|
|
|
fn frame(h: &mut Harness, event: Event) {
|
|
h.input_mut().events.push(event);
|
|
h.run_steps(1);
|
|
}
|
|
|
|
fn button(pos: Pos2, pressed: bool) -> Event {
|
|
Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }
|
|
}
|
|
|
|
fn press(h: &mut Harness, cell: &str) {
|
|
let from = point(h, cell);
|
|
frame(h, Event::PointerMoved(from));
|
|
frame(h, button(from, true));
|
|
}
|
|
|
|
fn click_with_modifiers(h: &mut Harness, cell: &str, modifiers: Modifiers) {
|
|
let pos = point(h, cell);
|
|
frame(h, Event::ModifiersChanged(modifiers));
|
|
frame(h, Event::PointerMoved(pos));
|
|
for pressed in [true, false] {
|
|
frame(h, Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers });
|
|
}
|
|
h.run_steps(2);
|
|
}
|
|
|
|
fn assert_selection(h: &Harness, start: &str, end: &str) {
|
|
let selection = &h.state().session.active().unwrap().selection;
|
|
let start = CellRef::parse(start).unwrap();
|
|
assert_eq!(selection.current(), RangeRef::new(start, CellRef::parse(end).unwrap()));
|
|
assert_eq!(selection.anchor, start);
|
|
assert_eq!(selection.active, start);
|
|
}
|
|
|
|
#[test]
|
|
fn single_movement_selects_from_the_pressed_cell_in_both_directions() {
|
|
for (initial, start, end) in [("A1", "C3", "F7"), ("C3", "C3", "F7"), ("A1", "F7", "C3"), ("F7", "F7", "C3")] {
|
|
let mut h = harness(initial);
|
|
let before = cells(&h);
|
|
press(&mut h, start);
|
|
let to = point(&h, end);
|
|
frame(&mut h, Event::PointerMoved(to));
|
|
assert_selection(&h, start, end);
|
|
frame(&mut h, button(to, false));
|
|
h.run_steps(1);
|
|
assert_selection(&h, start, end);
|
|
assert_eq!(cells(&h), before);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn release_consumes_a_new_endpoint_without_an_intermediate_movement_frame() {
|
|
let mut h = harness("A1");
|
|
let before = cells(&h);
|
|
press(&mut h, "C3");
|
|
let mid = point(&h, "D4");
|
|
frame(&mut h, Event::PointerMoved(mid));
|
|
let to = point(&h, "F7");
|
|
frame(&mut h, button(to, false));
|
|
assert_selection(&h, "C3", "F7");
|
|
assert_eq!(cells(&h), before);
|
|
}
|
|
|
|
#[test]
|
|
fn multiple_movement_frames_keep_the_anchor_and_cell_contents() {
|
|
let mut h = harness("A1");
|
|
let before = cells(&h);
|
|
press(&mut h, "C3");
|
|
for end in ["D4", "F7", "B2"] {
|
|
let to = point(&h, end);
|
|
frame(&mut h, Event::PointerMoved(to));
|
|
assert_selection(&h, "C3", end);
|
|
}
|
|
let to = point(&h, "B2");
|
|
frame(&mut h, button(to, false));
|
|
assert_selection(&h, "C3", "B2");
|
|
assert_eq!(cells(&h), before);
|
|
}
|
|
|
|
#[test]
|
|
fn clicking_then_hovering_does_not_extend_selection() {
|
|
let mut h = harness("A1");
|
|
let before = cells(&h);
|
|
press(&mut h, "C3");
|
|
let from = point(&h, "C3");
|
|
frame(&mut h, button(from, false));
|
|
let to = point(&h, "F7");
|
|
frame(&mut h, Event::PointerMoved(to));
|
|
assert_selection(&h, "C3", "C3");
|
|
assert_eq!(cells(&h), before);
|
|
}
|
|
|
|
#[test]
|
|
fn formula_point_mode_consumes_first_movement_and_release() {
|
|
let mut h = harness("A1");
|
|
let before = cells(&h);
|
|
h.state_mut().begin_edit(Some("=".into()), false);
|
|
h.run_steps(2);
|
|
press(&mut h, "C3");
|
|
let mid = point(&h, "D4");
|
|
frame(&mut h, Event::PointerMoved(mid));
|
|
assert_eq!(h.state().editor.as_ref().unwrap().text, "=C3:D4");
|
|
let to = point(&h, "F7");
|
|
frame(&mut h, button(to, false));
|
|
h.run_steps(1);
|
|
let editor = h.state().editor.as_ref().unwrap();
|
|
assert_eq!(editor.text, "=C3:F7");
|
|
assert_eq!(editor.point_cell, CellRef::parse("F7"));
|
|
assert_eq!(editor.point_anchor, Some((0, CellRef::parse("C3").unwrap())));
|
|
assert_eq!(cells(&h), before);
|
|
}
|
|
|
|
#[test]
|
|
fn formula_point_drag_keeps_the_sheet_qualifier() {
|
|
let mut h = harness("A1");
|
|
let before = cells(&h);
|
|
h.state_mut().session.execute("home.insertSheet", json!({"name": "Other sheet"})).unwrap();
|
|
h.state_mut().session.execute("sheet.activate", json!({"sheet": "Sheet1"})).unwrap();
|
|
h.state_mut().begin_edit(Some("=".into()), false);
|
|
h.state_mut().session.execute("sheet.activate", json!({"sheet": "Other sheet"})).unwrap();
|
|
h.run_steps(2);
|
|
let other_before = cells(&h);
|
|
press(&mut h, "C3");
|
|
let mid = point(&h, "D4");
|
|
frame(&mut h, Event::PointerMoved(mid));
|
|
assert_eq!(h.state().editor.as_ref().unwrap().text, "='Other sheet'!C3:D4");
|
|
let to = point(&h, "F7");
|
|
frame(&mut h, button(to, false));
|
|
assert_eq!(h.state().editor.as_ref().unwrap().text, "='Other sheet'!C3:F7");
|
|
assert_eq!(cells(&h), other_before);
|
|
h.state_mut().session.execute("sheet.activate", json!({"sheet": "Sheet1"})).unwrap();
|
|
assert_eq!(cells(&h), before);
|
|
}
|
|
|
|
#[test]
|
|
fn formula_modifier_command_click_appends_an_argument() {
|
|
let mut h = harness("J10");
|
|
let before = cells(&h);
|
|
h.state_mut().begin_edit(Some("=SUM(".into()), false);
|
|
h.run_steps(2);
|
|
click_with_modifiers(&mut h, "C3", Modifiers::NONE);
|
|
let command = Modifiers { mac_cmd: true, command: true, ..Modifiers::NONE };
|
|
click_with_modifiers(&mut h, "F7", command);
|
|
assert_eq!(h.state().editor.as_ref().unwrap().text, "=SUM(C3,F7");
|
|
assert_eq!(cells(&h), before);
|
|
}
|
|
|
|
#[test]
|
|
fn formula_modifier_shift_click_extends_the_pointed_range() {
|
|
let mut h = harness("J10");
|
|
let before = cells(&h);
|
|
h.state_mut().begin_edit(Some("=SUM(".into()), false);
|
|
h.run_steps(2);
|
|
click_with_modifiers(&mut h, "C3", Modifiers::NONE);
|
|
click_with_modifiers(&mut h, "F7", Modifiers::SHIFT);
|
|
assert_eq!(h.state().editor.as_ref().unwrap().text, "=SUM(C3:F7");
|
|
assert_eq!(cells(&h), before);
|
|
}
|
|
|
|
#[test]
|
|
fn formula_modifier_command_click_keeps_previous_arguments_replaceable_independently() {
|
|
for command in [Modifiers { mac_cmd: true, command: true, ..Modifiers::NONE }, Modifiers { ctrl: true, command: true, ..Modifiers::NONE }] {
|
|
let mut h = harness("J10");
|
|
let before = cells(&h);
|
|
h.state_mut().begin_edit(Some("=SUM(".into()), false);
|
|
h.run_steps(2);
|
|
click_with_modifiers(&mut h, "C3", Modifiers::NONE);
|
|
click_with_modifiers(&mut h, "D4", command);
|
|
click_with_modifiers(&mut h, "F7", command);
|
|
assert_eq!(h.state().editor.as_ref().unwrap().text, "=SUM(C3,D4,F7");
|
|
click_with_modifiers(&mut h, "G7", Modifiers::NONE);
|
|
assert_eq!(h.state().editor.as_ref().unwrap().text, "=SUM(C3,D4,G7");
|
|
click_with_modifiers(&mut h, "F9", Modifiers::SHIFT);
|
|
assert_eq!(h.state().editor.as_ref().unwrap().text, "=SUM(C3,D4,F7:G9");
|
|
assert_eq!(cells(&h), before);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn formula_modifier_shift_click_keeps_its_anchor_in_both_directions() {
|
|
for (start, first, second, expected) in [("C3", "F7", "B2", "B2:C3"), ("F7", "C3", "H9", "F7:H9")] {
|
|
let mut h = harness("J10");
|
|
let before = cells(&h);
|
|
h.state_mut().begin_edit(Some("=SUM(".into()), false);
|
|
h.run_steps(2);
|
|
click_with_modifiers(&mut h, start, Modifiers::NONE);
|
|
click_with_modifiers(&mut h, first, Modifiers::SHIFT);
|
|
click_with_modifiers(&mut h, second, Modifiers::SHIFT);
|
|
assert_eq!(h.state().editor.as_ref().unwrap().text, format!("=SUM({expected}"));
|
|
assert_eq!(cells(&h), before);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn formula_modifier_command_click_respects_locale_and_a_typed_separator() {
|
|
use gridcraft_engine::formula::FormulaLocale;
|
|
let command = Modifiers { mac_cmd: true, command: true, ..Modifiers::NONE };
|
|
for (locale, prefix, separator) in [(FormulaLocale::En, "=SUM(", ","), (FormulaLocale::Es, "=SUMA(", ";")] {
|
|
let mut h = harness("J10");
|
|
h.state_mut().begin_edit(Some(prefix.into()), false);
|
|
h.state_mut().editor.as_mut().unwrap().locale = locale;
|
|
h.run_steps(2);
|
|
click_with_modifiers(&mut h, "C3", command);
|
|
assert_eq!(h.state().editor.as_ref().unwrap().text, format!("{prefix}C3"));
|
|
click_with_modifiers(&mut h, "F7", command);
|
|
assert_eq!(h.state().editor.as_ref().unwrap().text, format!("{prefix}C3{separator}F7"));
|
|
frame(&mut h, Event::ModifiersChanged(Modifiers::NONE));
|
|
frame(&mut h, Event::Text(separator.into()));
|
|
h.run_steps(2);
|
|
assert!(h.state().editor.as_ref().unwrap().point_anchor.is_none());
|
|
click_with_modifiers(&mut h, "G8", command);
|
|
assert_eq!(h.state().editor.as_ref().unwrap().text, format!("{prefix}C3{separator}F7{separator}G8"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn formula_modifier_shift_click_does_not_reuse_an_anchor_from_another_sheet() {
|
|
let mut h = harness("J10");
|
|
h.state_mut().session.execute("home.insertSheet", json!({"name": "Other sheet"})).unwrap();
|
|
h.state_mut().session.execute("sheet.activate", json!({"sheet": "Sheet1"})).unwrap();
|
|
h.state_mut().begin_edit(Some("=SUM(".into()), false);
|
|
h.run_steps(2);
|
|
click_with_modifiers(&mut h, "C3", Modifiers::NONE);
|
|
h.state_mut().session.execute("sheet.activate", json!({"sheet": "Other sheet"})).unwrap();
|
|
h.run_steps(2);
|
|
click_with_modifiers(&mut h, "F7", Modifiers::SHIFT);
|
|
assert_eq!(h.state().editor.as_ref().unwrap().text, "=SUM('Other sheet'!F7");
|
|
click_with_modifiers(&mut h, "D4", Modifiers::SHIFT);
|
|
assert_eq!(h.state().editor.as_ref().unwrap().text, "=SUM('Other sheet'!D4:F7");
|
|
}
|
|
|
|
#[test]
|
|
fn formula_modifier_shift_click_then_arrow_moves_from_the_clicked_endpoint() {
|
|
use egui::Key;
|
|
for (modifiers, expected) in [(Modifiers::SHIFT, "=SUM(C3:G7"), (Modifiers::NONE, "=SUM(G7")] {
|
|
let mut h = harness("J10");
|
|
h.state_mut().begin_edit(Some("=SUM(".into()), false);
|
|
h.run_steps(2);
|
|
click_with_modifiers(&mut h, "C3", Modifiers::NONE);
|
|
click_with_modifiers(&mut h, "F7", Modifiers::SHIFT);
|
|
frame(&mut h, Event::ModifiersChanged(modifiers));
|
|
for pressed in [true, false] {
|
|
frame(&mut h, Event::Key { key: Key::ArrowRight, physical_key: None, pressed, repeat: false, modifiers });
|
|
}
|
|
h.run_steps(2);
|
|
assert_eq!(h.state().editor.as_ref().unwrap().text, expected);
|
|
assert_eq!(h.state().editor.as_ref().unwrap().point_cell, CellRef::parse("G7"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn formula_arrow_pointing_restarts_from_the_edited_cell_after_typing() {
|
|
let mut h = harness("B4");
|
|
h.state_mut().begin_edit(Some("=".into()), false);
|
|
h.run_steps(2);
|
|
h.key_press(egui::Key::ArrowUp);
|
|
h.run_steps(1);
|
|
h.key_press(egui::Key::ArrowUp);
|
|
h.run_steps(1);
|
|
assert_eq!(h.state().editor.as_ref().unwrap().text, "=B2");
|
|
frame(&mut h, Event::Text("+".into()));
|
|
h.run_steps(1);
|
|
h.key_press(egui::Key::ArrowUp);
|
|
h.run_steps(1);
|
|
assert_eq!(h.state().editor.as_ref().unwrap().text, "=B2+B3");
|
|
}
|