Files
gridcraft/crates/ui-egui/tests/it/function_library.rs
T
prxnvt 6aa5a78828 UI: Formulas › Function Library as labelled buttons with category menus
The Function Library group was a big Insert Function button followed by
AutoSum and seven big category buttons that all drew the same icon, the
widest group on the Formulas tab. It is now laid out as Excel lays it
out when the window is not wide enough for a big button per category:
big Insert Function and AutoSum buttons, then the categories as
labelled small buttons in columns of three:

  Recently Used    Text                 Math & Trig
  Financial        Date & Time          More Functions
  Logical          Lookup & Reference

- The category buttons draw a new code-drawn icon, Icon::FnBook, a book
  whose spine and glyph (star, coins, question mark, letter, clock,
  magnifier, theta, dots) take the category's colour from the theme's
  category palette. icons::paint_accent draws an icon with an accent
  colour, and widgets::small_button_accent is a small button that uses
  it; small_button is unchanged for its callers.
- AutoSum stays the big split button: the icon inserts SUM, the label
  and arrow open the AutoSum menu, which now reads Sum, Average, Count
  Numbers, Max, Min and More Functions… and is shared with Home ›
  Editing. Its tooltip names the control only, so the shortcut comes
  from the platform lookup (Alt+= on Windows and Linux).
- Each category menu lists its functions with their descriptions on
  hover and ends with Insert Function…, which opens the dialog with
  that category. More Functions cascades into Statistical, Engineering,
  Cube, Information, Compatibility and Web. Recently Used lists what
  the engine's formulas.recentlyUsed returns.
- The function categories, the function list (built once) and the menus
  live in a new function_library module; the dialog's category labels
  come from it.
- keytips.json gains MR for Recently Used, Excel's sequence. Labelled
  small and big buttons report their label to accessibility.
- The one-line labels (Date & Time, Lookup & Reference, Math & Trig,
  More Functions) replace the two-line keys in every interface
  language, with the same words. Recently Used is translated in Czech,
  German, Spanish, French, Japanese, Korean, Portuguese, Russian and
  Chinese; the other languages show it in English, their fallback.

Tests: tests/it/function_library.rs covers the layout (columns of three
level with the tab's other columns), each category menu and its Insert
Function… row, the More Functions cascade, the AutoSum split button and
its menu, and the MR key tip. A ribbon unit test keeps Command-key
glyphs out of the ribbon's text. chart_resize picks the chart menu's
Delete now that Home › Cells' Delete has an accessible name.
2026-10-31 03:35:06 -04:00

172 lines
7.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Formulas › Function Library: the labelled buttons, their category menus, the More Functions
//! cascade, the AutoSum big split button and the Recently Used keytip.
use egui::os::OperatingSystem;
use egui::{Event, Key, Modifiers, vec2};
use egui_kittest::kittest::Queryable as _;
use gridcraft_engine::Session;
use gridcraft_engine::core::CellRef;
use gridcraft_ui_egui::SheetApp;
use serde_json::json;
type Harness = egui_kittest::Harness<'static, SheetApp>;
fn formulas_tab(width: f32) -> Harness {
let mut session = Session::new();
session.new_workbook();
let mut app = crate::common::app(session);
app.ui.ribbon_tab = "Formulas".into();
crate::common::harness_with(crate::common::builder().with_os(OperatingSystem::Nix).with_size(vec2(width, 760.0)), app, 4)
}
fn editor_text(h: &Harness) -> Option<String> {
h.state().editor.as_ref().map(|e| e.text.clone())
}
fn input(h: &Harness, cell: &str) -> String {
let d = h.state().session.active().unwrap();
d.wb.active().unwrap().input_text(CellRef::parse(cell).unwrap())
}
/// Clicks a button by its name (a tooltip also names an icon-only button, such as the formula
/// bar's fx, which is not a button to accessibility).
fn click(h: &mut Harness, label: &str) {
h.get_by_role_and_label(egui::accesskit::Role::Button, label).click();
h.run_steps(3);
}
/// Clicks a menu row through accessibility, which reaches rows scrolled out of a long menu.
fn pick(h: &mut Harness, label: &str) {
h.get_by_label(label).click_accesskit();
h.run_steps(3);
}
fn close_menus(h: &mut Harness) {
h.key_press(Key::Escape);
h.run_steps(2);
h.state_mut().editor = None;
h.run_steps(2);
}
#[test]
fn autosum_is_a_big_split_button_then_the_categories_in_columns_of_three() {
let h = formulas_tab(1440.0);
let columns: [&[&str]; 3] =
[&["Recently Used", "Financial", "Logical"], &["Text", "Date & Time", "Lookup & Reference"], &["Math & Trig", "More Functions"]];
let rect = |l: &str| h.get_by_role_and_label(egui::accesskit::Role::Button, l).rect();
for column in columns {
for pair in column.windows(2) {
let (a, b) = (rect(pair[0]), rect(pair[1]));
assert!((a.left() - b.left()).abs() < 0.5, "{pair:?} share a left edge");
assert!(a.bottom() <= b.top() + 0.5, "{pair:?} stack in order");
}
}
for pair in columns.windows(2) {
assert!(rect(pair[0][0]).right() <= rect(pair[1][0]).left(), "columns run left to right");
}
// Insert Function and AutoSum are big buttons, to the left of the columns.
let (insert, sum) = (rect("Insert Function"), rect("AutoSum"));
assert!(insert.right() <= sum.left() && sum.right() <= rect("Recently Used").left());
assert!(sum.top() <= rect("Recently Used").top() + 0.5, "AutoSum starts at the top of the group");
assert!(h.get_by_label("AutoSum options").rect().bottom() > rect("Financial").bottom(), "its arrow part reaches below the second row");
// A full column stands where the tab's other columns of three small buttons do (Formula
// Auditing's), so it fits the ribbon in any font.
let (top, bottom) = (rect("Show Formulas").top(), rect("Evaluate Formula").bottom());
for column in &columns[..2] {
let (t, b) = (rect(column[0]).top(), rect(column[2]).bottom());
assert!((t - top).abs() < 0.5 && (b - bottom).abs() < 0.5, "{column:?} spans {t}..{b}, Formula Auditing {top}..{bottom}");
}
}
#[test]
fn each_category_menu_lists_its_functions_then_insert_function() {
let mut h = formulas_tab(1440.0);
// Recently Used lists the functions of formulas entered.
h.state_mut().run("cell.set", json!({"cell": "A1", "input": "=SUM(1)+AVERAGE(2)"})).unwrap();
h.run_steps(2);
for (button, function) in [
("Recently Used", "SUM"),
("Financial", "PMT"),
("Logical", "IF"),
("Text", "CONCAT"),
("Date & Time", "TODAY"),
("Lookup & Reference", "XLOOKUP"),
("Math & Trig", "ROUND"),
] {
click(&mut h, button);
h.get_by_label("Insert Function…");
pick(&mut h, function);
assert_eq!(editor_text(&h).as_deref(), Some(format!("={function}(").as_str()), "{button} › {function} starts the formula");
close_menus(&mut h);
}
// The menu's last row opens Insert Function on the menu's category.
click(&mut h, "Date & Time");
click(&mut h, "Insert Function…");
let d = h.state().dialog.as_ref().expect("Insert Function opens");
assert_eq!(d.name(), "insertFunction");
assert_eq!(d.values.get("category"), Some(&json!("Date & Time")));
}
#[test]
fn more_functions_cascades_into_the_remaining_categories() {
let mut h = formulas_tab(1440.0);
click(&mut h, "More Functions");
for c in ["Statistical", "Engineering", "Cube", "Information", "Compatibility", "Web"] {
h.get_by_label(c);
}
click(&mut h, "Engineering");
h.get_by_label("Insert Function…");
pick(&mut h, "BIN2DEC");
assert_eq!(editor_text(&h).as_deref(), Some("=BIN2DEC("));
close_menus(&mut h);
click(&mut h, "More Functions");
click(&mut h, "Statistical");
pick(&mut h, "Insert Function…");
assert_eq!(h.state().dialog.as_ref().and_then(|d| d.values.get("category")), Some(&json!("Statistical")));
}
#[test]
fn autosum_runs_sum_and_its_arrow_opens_the_shared_menu() {
let mut h = formulas_tab(1440.0);
{
let s = &mut h.state_mut().session;
s.execute("range.setValues", json!({"range": "A1", "values": [[1, 4], [2, 5], [3, 6]]})).unwrap();
s.execute("selection.set", json!({"cell": "A4"})).unwrap();
}
h.run_steps(2);
click(&mut h, "AutoSum");
assert_eq!(input(&h, "A4"), "=SUM(A1:A3)");
h.state_mut().session.execute("selection.set", json!({"cell": "B4"})).unwrap();
click(&mut h, "AutoSum options");
for item in ["Sum", "Average", "Max", "Min", "More Functions…"] {
h.get_by_label(item);
}
click(&mut h, "Count Numbers");
assert_eq!(input(&h, "B4"), "=COUNT(B1:B3)");
click(&mut h, "AutoSum options");
click(&mut h, "More Functions…");
assert_eq!(h.state().dialog.as_ref().map(|d| d.name().to_string()).as_deref(), Some("insertFunction"));
}
fn key_event(key: Key, pressed: bool) -> Event {
Event::Key { key, physical_key: None, pressed, repeat: false, modifiers: Modifiers::NONE }
}
#[test]
fn the_recently_used_keytip_opens_its_menu() {
let mut h = formulas_tab(1440.0);
// Recently Used lists the functions of formulas entered.
h.state_mut().run("cell.set", json!({"cell": "A1", "input": "=SUM(1)+AVERAGE(2)"})).unwrap();
h.run_steps(2);
h.input_mut().events.extend([key_event(Key::F10, true), key_event(Key::F10, false)]);
h.run_steps(2);
for k in [Key::M, Key::R] {
h.input_mut().events.extend([key_event(k, true), Event::Text(k.name().to_ascii_lowercase()), key_event(k, false)]);
h.run_steps(3);
}
assert_eq!(h.state().keytips.prefix(), None, "MR finishes the sequence");
h.get_by_label("Insert Function…");
pick(&mut h, "AVERAGE");
assert_eq!(editor_text(&h).as_deref(), Some("=AVERAGE("));
}