mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-02 07:06:48 -06:00
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.
This commit is contained in:
+3
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~25 points of "ready for real work" and ~160–240 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-25 · **Change:** minor (AutoSum over a selected row puts its total to the right, and fills a selection's empty last row and column; earlier: macro engine and `macro.*` commands; macro interface on the Automate tab) · **Target:** Microsoft Excel (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-31 · **Change:** minor (Formulas › Function Library as labelled buttons with category menus; earlier: AutoSum over a selected row puts its total to the right, and fills a selection's empty last row and column) · **Target:** Microsoft Excel (Microsoft 365)
|
||||
|
||||
GridCraft aims at full Microsoft Excel parity, and at being better in places: faster, open (XLSX
|
||||
is the native format), fully scriptable by agents (MCP, control channel, CLI), and on the web.
|
||||
@@ -135,6 +135,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
||||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-31 | Formulas › Function Library as a big Insert Function button, the AutoSum split button and three columns of labelled small buttons, each category book in its own colour; category menus end with Insert Function… on that category; More Functions cascades into the remaining categories; Home › Editing shares the AutoSum menu; the MR key tip opens Recently Used |
|
||||
| 2026-10-25 | AutoSum over a selection does what Excel does: a selected row gets one total to its right (in its last cell if that is empty), not a one-cell total under each cell; a block whose last column is empty gets each row's total there, one whose last row is empty each column's, and with both the corner gets the total of the block; otherwise totals go under each column as before |
|
||||
| 2026-10-24 | Macro interface ([macros.md](docs/macros.md#in-the-window)): an Automate › Macros group (Record Macro, which turns into Stop Recording, Use Relative References, Run Macro with recent macros); "Recording · N steps" with Stop in the status bar; a Record Macro dialog (checked name, free Ctrl+Shift shortcut, description); the Macros dialog lists macros beside Automate scripts and a VBA project, with Run, Step Into, Edit, Create, Delete and Options; Ctrl+Shift+letter macro shortcuts; questions that offer Run, Overwrite, Shift Down or Shift Right; a step editor with a breakpoint gutter, drag to reorder, a command picker, reference boxes that switch between relative tokens and absolute references, Inputs and Output tabs, a debug bar (Check, Run, Step, Continue, Run to Here, Stop with keep or undo) and grid overlays for inputs, writes, conflicts and the current step |
|
||||
| 2026-10-24 | Go To lists the named ranges of every sheet (`Sheet2!Name` for a name scoped to another sheet; hidden names, constants and formulas left out) and the references entered before, a double-click goes to an entry, and the Reference box takes several names or references separated by commas (`Price, Type`, `B14:C22,F19:G30`) |
|
||||
@@ -244,6 +245,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-31 | minor | Progress log: Function Library |
|
||||
| 2026-10-25 | minor | Progress log: AutoSum over a selected row, and a selection's empty last row and column |
|
||||
| 2026-10-24 | minor | Macro interface on the Automate tab; progress log row |
|
||||
| 2026-10-24 | minor | Macro engine and `macro.*` commands; catalog 275 / 291 → 276 / 292 (View › Macros › Record Macro); progress log row |
|
||||
|
||||
@@ -853,7 +853,11 @@ impl Dialog {
|
||||
}
|
||||
d
|
||||
}
|
||||
"insertFunction" => Dialog::custom("insertFunction", "Formula Builder", json!({"category": "Most Recently Used"})),
|
||||
"insertFunction" => {
|
||||
// A Function Library menu opens it on its own category.
|
||||
let category = p("category").as_str().unwrap_or(crate::function_library::MOST_RECENTLY_USED).to_string();
|
||||
Dialog::custom("insertFunction", "Formula Builder", json!({"category": category}))
|
||||
}
|
||||
"find" => {
|
||||
let mut d = Dialog::custom("find", "Find and Replace", json!({"what": "", "with": "", "within": "sheet", "lookIn": "formulas"}));
|
||||
d.tab = if p("replace").as_bool() == Some(true) { "Replace".into() } else { "Find".into() };
|
||||
|
||||
@@ -26,7 +26,7 @@ pub(super) fn insert_function(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dia
|
||||
egui::ScrollArea::vertical().max_height(260.0).show(ui, |ui| {
|
||||
for (i, f) in hits.iter().enumerate() {
|
||||
let name = locale.function_name(f["name"].as_str().unwrap_or(""));
|
||||
let category = language.tr(function_category_label(f["category"].as_str().unwrap_or("")));
|
||||
let category = language.tr(crate::function_library::category_label(f["category"].as_str().unwrap_or("")));
|
||||
let r = ui.add(egui::Button::selectable(i == d.list_index, format!("{name:<18} {category}")).min_size(vec2(480.0, 20.0)));
|
||||
if r.clicked() {
|
||||
d.list_index = i;
|
||||
@@ -58,15 +58,3 @@ pub(super) fn insert_function(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dia
|
||||
*confirm = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Excel's name for a function category, given the catalog's id (`MathTrig`).
|
||||
fn function_category_label(id: &str) -> &str {
|
||||
match id {
|
||||
"DateTime" => "Date & Time",
|
||||
"Lookup" => "Lookup & Reference",
|
||||
"MathTrig" => "Math & Trig",
|
||||
"AddIn" => "Add-in and Automation",
|
||||
// Financial, Logical, Text, Statistical, Engineering, Database, Information, …
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
//! The Formulas tab's Function Library: the function categories, their menus, the AutoSum menu
|
||||
//! (shared with Home › Editing), and the Recently Used menu, which lists what the engine's
|
||||
//! `formulas.recentlyUsed` returns.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use egui::{Ui, vec2};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::SheetApp;
|
||||
|
||||
/// The Insert Function category that lists [`recent`].
|
||||
pub const MOST_RECENTLY_USED: &str = "Most Recently Used";
|
||||
/// The Insert Function category that lists every function.
|
||||
pub const ALL: &str = "All";
|
||||
|
||||
/// The function categories in the order the Insert Function dialog lists them: (the engine's
|
||||
/// category id, the label people see).
|
||||
pub const CATEGORIES: [(&str, &str); 14] = [
|
||||
("Financial", "Financial"),
|
||||
("DateTime", "Date & Time"),
|
||||
("MathTrig", "Math & Trig"),
|
||||
("Statistical", "Statistical"),
|
||||
("Lookup", "Lookup & Reference"),
|
||||
("Database", "Database"),
|
||||
("Text", "Text"),
|
||||
("Logical", "Logical"),
|
||||
("Information", "Information"),
|
||||
("Engineering", "Engineering"),
|
||||
("Cube", "Cube"),
|
||||
("Compatibility", "Compatibility"),
|
||||
("Web", "Web"),
|
||||
("AddIn", "Add-in and Automation"),
|
||||
];
|
||||
|
||||
/// The categories under More Functions, each a cascading submenu.
|
||||
pub const MORE_FUNCTIONS: [&str; 6] = ["Statistical", "Engineering", "Cube", "Information", "Compatibility", "Web"];
|
||||
|
||||
/// The label of an engine category id (the id itself if it is unknown).
|
||||
pub fn category_label(id: &str) -> &str {
|
||||
CATEGORIES.iter().find(|(c, _)| *c == id).map(|(_, l)| *l).unwrap_or(id)
|
||||
}
|
||||
|
||||
/// The engine category id for a category given by id or label, in any case.
|
||||
pub fn category_id(name: &str) -> Option<&'static str> {
|
||||
CATEGORIES.iter().find(|(c, l)| c.eq_ignore_ascii_case(name) || l.eq_ignore_ascii_case(name)).map(|(c, _)| *c)
|
||||
}
|
||||
|
||||
/// One function of the library.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FnEntry {
|
||||
pub name: String,
|
||||
pub category: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// Every function, sorted by name (built once; the list is static).
|
||||
pub fn all() -> &'static [FnEntry] {
|
||||
static LIST: OnceLock<Vec<FnEntry>> = OnceLock::new();
|
||||
LIST.get_or_init(|| {
|
||||
let mut list: Vec<FnEntry> = gridcraft_engine::cmd::formulas::function_list()
|
||||
.iter()
|
||||
.filter_map(|f| {
|
||||
Some(FnEntry {
|
||||
name: f["name"].as_str()?.to_string(),
|
||||
category: f["category"].as_str().unwrap_or("").to_string(),
|
||||
description: f["description"].as_str().unwrap_or("").to_string(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
list.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
list.dedup_by(|a, b| a.name == b.name);
|
||||
list
|
||||
})
|
||||
}
|
||||
|
||||
/// The function named `name` (upper case), if the library knows it.
|
||||
pub fn find(name: &str) -> Option<&'static FnEntry> {
|
||||
let list = all();
|
||||
list.binary_search_by(|f| f.name.as_str().cmp(name)).ok().and_then(|i| list.get(i))
|
||||
}
|
||||
|
||||
/// The functions of one engine category, in name order.
|
||||
pub fn in_category(id: &str) -> impl Iterator<Item = &'static FnEntry> + '_ {
|
||||
all().iter().filter(move |f| f.category == id)
|
||||
}
|
||||
|
||||
/// The name a function shows under in the interface language.
|
||||
fn display_name<'a>(app: &SheetApp, name: &'a str) -> &'a str {
|
||||
let locale = app.ui.language.formula_locale();
|
||||
app.session.active().map(|d| crate::formula_locale::completion_name(name, locale, &d.wb, d.wb.active_sheet())).unwrap_or(name)
|
||||
}
|
||||
|
||||
/// One menu row per function, with its description on hover; a click starts the formula.
|
||||
fn function_items<'a>(app: &mut SheetApp, ui: &mut Ui, names: impl Iterator<Item = &'a str>) {
|
||||
let language = app.ui.language;
|
||||
let locale = language.formula_locale();
|
||||
ui.set_max_height(420.0);
|
||||
egui::ScrollArea::vertical().max_height(380.0).show(ui, |ui| {
|
||||
for n in names {
|
||||
let Some(entry) = find(n) else { continue };
|
||||
let name = display_name(app, &entry.name);
|
||||
let r = ui.add(egui::Button::new(name).frame(false).min_size(vec2(180.0, 20.0)));
|
||||
// The description is looked up only while the row is hovered.
|
||||
let r = r.on_hover_ui(|ui| {
|
||||
ui.label(crate::formula_locale::description(&entry.name, locale, language).unwrap_or_default());
|
||||
});
|
||||
if r.clicked() {
|
||||
app.run_or_alert("formulas.insertFunction", json!({"name": entry.name}));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The "Insert Function…" row that ends every category menu; it opens the dialog on `category`.
|
||||
fn insert_function_item(app: &mut SheetApp, ui: &mut Ui, category: &str) {
|
||||
ui.separator();
|
||||
let label = app.ui.language.tr("Insert Function…");
|
||||
if ui.add(egui::Button::new(label).frame(false).min_size(vec2(180.0, 22.0))).clicked() {
|
||||
app.open_dialog("insertFunction", json!({"category": category}));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// The menu of one category: its functions, then Insert Function….
|
||||
pub fn category_menu(app: &mut SheetApp, ui: &mut Ui, id: &str) {
|
||||
let names: Vec<&str> = in_category(id).map(|f| f.name.as_str()).collect();
|
||||
function_items(app, ui, names.into_iter());
|
||||
insert_function_item(app, ui, category_label(id));
|
||||
}
|
||||
|
||||
/// The functions `formulas.recentlyUsed` lists, newest first (none without a workbook).
|
||||
pub fn recent(app: &mut SheetApp) -> Vec<String> {
|
||||
let list = app.session.execute("formulas.recentlyUsed", json!({})).unwrap_or_default();
|
||||
list.as_array().map(|a| a.iter().filter_map(|n| n.as_str().map(String::from)).collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Recently Used: the most recent functions, newest first, then Insert Function….
|
||||
pub fn recent_menu(app: &mut SheetApp, ui: &mut Ui) {
|
||||
let recent = recent(app);
|
||||
function_items(app, ui, recent.iter().map(String::as_str));
|
||||
insert_function_item(app, ui, MOST_RECENTLY_USED);
|
||||
}
|
||||
|
||||
/// More Functions: a cascading submenu per remaining category.
|
||||
pub fn more_menu(app: &mut SheetApp, ui: &mut Ui) {
|
||||
for id in MORE_FUNCTIONS {
|
||||
let label = app.ui.language.tr(category_label(id)).to_string();
|
||||
ui.menu_button(label, |ui| category_menu(app, ui, id));
|
||||
}
|
||||
}
|
||||
|
||||
/// The AutoSum menu, shared by Home › Editing and Formulas › Function Library: a function, or
|
||||
/// the Insert Function dialog.
|
||||
pub fn auto_sum_menu(app: &mut SheetApp, ui: &mut Ui) {
|
||||
for (label, f) in [("Sum", "SUM"), ("Average", "AVERAGE"), ("Count Numbers", "COUNT"), ("Max", "MAX"), ("Min", "MIN")] {
|
||||
let label = app.ui.language.tr(label);
|
||||
if ui.add(egui::Button::new(label).frame(false).min_size(vec2(160.0, 22.0))).clicked() {
|
||||
app.run_or_alert("formulas.autoSum", json!({"function": f}));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
let label = app.ui.language.tr("More Functions…");
|
||||
if ui.add(egui::Button::new(label).frame(false).min_size(vec2(160.0, 22.0))).clicked() {
|
||||
app.open_dialog("insertFunction", json!({}));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn categories_cover_every_function() {
|
||||
for f in all() {
|
||||
assert!(category_id(&f.category).is_some(), "{} is in an unknown category {}", f.name, f.category);
|
||||
}
|
||||
assert_eq!(category_id("date & time"), Some("DateTime"));
|
||||
assert_eq!(category_id("Lookup"), Some("Lookup"));
|
||||
assert_eq!(category_id("nope"), None);
|
||||
for c in MORE_FUNCTIONS {
|
||||
assert!(category_id(c).is_some());
|
||||
}
|
||||
assert!(all().windows(2).all(|w| w[0].name < w[1].name), "sorted by name, without duplicates");
|
||||
assert_eq!(find("SUM").map(|f| f.category.as_str()), Some("MathTrig"));
|
||||
assert!(find("NOT_A_FUNCTION").is_none());
|
||||
}
|
||||
}
|
||||
@@ -417,11 +417,12 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Long Date" => "Dlouhé datum",
|
||||
"More Number Formats…" => "Další formáty čísel…",
|
||||
"Financial" => "Finanční",
|
||||
"Recently Used" => "Naposledy použité",
|
||||
"Logical" => "Logické",
|
||||
"Date &\nTime" => "Datum a\nčas",
|
||||
"Lookup &\nReference" => "Vyhledávání\na odkazy",
|
||||
"Math &\nTrig" => "Matematika a\ntrigonometrie",
|
||||
"More\nFunctions" => "Další\nfunkce",
|
||||
"Date & Time" => "Datum a čas",
|
||||
"Lookup & Reference" => "Vyhledávání a odkazy",
|
||||
"Math & Trig" => "Matematika a trigonometrie",
|
||||
"More Functions" => "Další funkce",
|
||||
"Sheet Options|View" => "Zobrazit",
|
||||
"Print" => "Tisknout",
|
||||
"Print What" => "Co tisknout",
|
||||
|
||||
@@ -420,11 +420,12 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Long Date" => "Langes Datum",
|
||||
"More Number Formats…" => "Weitere Zahlenformate…",
|
||||
"Financial" => "Finanzmathematik",
|
||||
"Recently Used" => "Zuletzt verwendet",
|
||||
"Logical" => "Logisch",
|
||||
"Date &\nTime" => "Datum &\nUhrzeit",
|
||||
"Lookup &\nReference" => "Nachschlagen &\nVerweisen",
|
||||
"Math &\nTrig" => "Mathematik &\nTrigonometrie",
|
||||
"More\nFunctions" => "Weitere\nFunktionen",
|
||||
"Date & Time" => "Datum & Uhrzeit",
|
||||
"Lookup & Reference" => "Nachschlagen & Verweisen",
|
||||
"Math & Trig" => "Mathematik & Trigonometrie",
|
||||
"More Functions" => "Weitere Funktionen",
|
||||
"Page Layout|Orientation" => "Seitenausrichtung",
|
||||
"Display theme" => "Anzeigedesign",
|
||||
"System" => "System",
|
||||
|
||||
@@ -389,10 +389,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"More Number Formats…" => "Περισσότερες μορφές αριθμών…",
|
||||
"Financial" => "Οικονομικές",
|
||||
"Logical" => "Λογικές",
|
||||
"Date &\nTime" => "Ημερομηνία και\nώρα",
|
||||
"Lookup &\nReference" => "Αναζήτηση και\nαναφορά",
|
||||
"Math &\nTrig" => "Μαθηματικά και\nτριγωνομετρία",
|
||||
"More\nFunctions" => "Περισσότερες\nσυναρτήσεις",
|
||||
"More Functions" => "Περισσότερες συναρτήσεις",
|
||||
"Page Layout|Orientation" => "Προσανατολισμός σελίδας",
|
||||
"Display theme" => "Θέμα εμφάνισης",
|
||||
"System" => "Σύστημα",
|
||||
|
||||
@@ -417,11 +417,12 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Long Date" => "Fecha larga",
|
||||
"More Number Formats…" => "Más formatos de número…",
|
||||
"Financial" => "Financieras",
|
||||
"Recently Used" => "Usadas recientemente",
|
||||
"Logical" => "Lógicas",
|
||||
"Date &\nTime" => "Fecha\ny hora",
|
||||
"Lookup &\nReference" => "Búsqueda y\nreferencia",
|
||||
"Math &\nTrig" => "Matemáticas y\ntrigonométricas",
|
||||
"More\nFunctions" => "Más\nfunciones",
|
||||
"Date & Time" => "Fecha y hora",
|
||||
"Lookup & Reference" => "Búsqueda y referencia",
|
||||
"Math & Trig" => "Matemáticas y trigonométricas",
|
||||
"More Functions" => "Más funciones",
|
||||
"Page Layout|Orientation" => "Orientación",
|
||||
"Display theme" => "Tema de la interfaz",
|
||||
"System" => "Sistema",
|
||||
|
||||
@@ -308,11 +308,12 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Long Date" => "Date longue",
|
||||
"More Number Formats…" => "Autres formats numériques…",
|
||||
"Financial" => "Financières",
|
||||
"Recently Used" => "Récemment utilisées",
|
||||
"Logical" => "Logiques",
|
||||
"Date &\nTime" => "Date et\nheure",
|
||||
"Lookup &\nReference" => "Recherche et\nréférence",
|
||||
"Math &\nTrig" => "Maths et\ntrigonométrie",
|
||||
"More\nFunctions" => "Autres\nfonctions",
|
||||
"Date & Time" => "Date et heure",
|
||||
"Lookup & Reference" => "Recherche et référence",
|
||||
"Math & Trig" => "Maths et trigonométrie",
|
||||
"More Functions" => "Autres fonctions",
|
||||
"Sheet Options|View" => "Affichage",
|
||||
"Print" => "Impression",
|
||||
"Print What" => "Quoi imprimer",
|
||||
|
||||
@@ -291,10 +291,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"More Number Formats…" => "Altri formati numero…",
|
||||
"Financial" => "Finanziarie",
|
||||
"Logical" => "Logiche",
|
||||
"Date &\nTime" => "Data e\nora",
|
||||
"Lookup &\nReference" => "Ricerca e\nriferimento",
|
||||
"Math &\nTrig" => "Matematiche e\ntrigonometriche",
|
||||
"More\nFunctions" => "Altre\nfunzioni",
|
||||
"More Functions" => "Altre funzioni",
|
||||
"Page Layout|Orientation" => "Orientamento",
|
||||
"Display theme" => "Tema di visualizzazione",
|
||||
"Animations" => "Animazioni",
|
||||
|
||||
@@ -304,10 +304,8 @@ pub(super) const CORE: &[(&str, &str)] = &[
|
||||
("More Number Formats…", "その他の表示形式…"),
|
||||
("Financial", "財務"),
|
||||
("Logical", "論理"),
|
||||
("Date &\nTime", "日付/時刻"),
|
||||
("Lookup &\nReference", "検索/行列"),
|
||||
("Math &\nTrig", "数学/三角"),
|
||||
("More\nFunctions", "その他の関数"),
|
||||
("Recently Used", "最近使用した関数"),
|
||||
("More Functions", "その他の関数"),
|
||||
("Sheet Options|View", "画面"),
|
||||
("Print", "印刷"),
|
||||
("Print What", "印刷する内容"),
|
||||
|
||||
@@ -295,11 +295,12 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Long Date" => "자세한 날짜",
|
||||
"More Number Formats…" => "숫자 서식 더 보기…",
|
||||
"Financial" => "재무",
|
||||
"Recently Used" => "최근 사용 항목",
|
||||
"Logical" => "논리",
|
||||
"Date &\nTime" => "날짜 및\n시간",
|
||||
"Lookup &\nReference" => "조회 및\n참조",
|
||||
"Math &\nTrig" => "수학 및\n삼각",
|
||||
"More\nFunctions" => "기타\n함수",
|
||||
"Date & Time" => "날짜 및 시간",
|
||||
"Lookup & Reference" => "조회 및 참조",
|
||||
"Math & Trig" => "수학 및 삼각",
|
||||
"More Functions" => "기타 함수",
|
||||
"Page Layout|Orientation" => "용지 방향",
|
||||
"Display theme" => "표시 테마",
|
||||
"Animations" => "애니메이션",
|
||||
|
||||
@@ -300,10 +300,10 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"More Number Formats…" => "Meer getalnotaties…",
|
||||
"Financial" => "Financieel",
|
||||
"Logical" => "Logisch",
|
||||
"Date &\nTime" => "Datum en\ntijd",
|
||||
"Lookup &\nReference" => "Zoeken en\nverwijzen",
|
||||
"Math &\nTrig" => "Wiskunde en\ntrigonometrie",
|
||||
"More\nFunctions" => "Meer\nfuncties",
|
||||
"Date & Time" => "Datum en tijd",
|
||||
"Lookup & Reference" => "Zoeken en verwijzen",
|
||||
"Math & Trig" => "Wiskunde en trigonometrie",
|
||||
"More Functions" => "Meer functies",
|
||||
"Sheet Options|View" => "Weergeven",
|
||||
"Print" => "Afdrukken",
|
||||
"Print What" => "Wat afdrukken",
|
||||
|
||||
@@ -287,10 +287,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"More Number Formats…" => "Więcej formatów liczb…",
|
||||
"Financial" => "Finansowe",
|
||||
"Logical" => "Logiczne",
|
||||
"Date &\nTime" => "Data i\ngodzina",
|
||||
"Lookup &\nReference" => "Wyszukiwanie\ni odwołania",
|
||||
"Math &\nTrig" => "Matematyczne i\ntrygonometryczne",
|
||||
"More\nFunctions" => "Więcej\nfunkcji",
|
||||
"More Functions" => "Więcej funkcji",
|
||||
"Page Layout|Orientation" => "Orientacja",
|
||||
"Display theme" => "Motyw wyświetlania",
|
||||
"Animations" => "Animacje",
|
||||
|
||||
@@ -291,11 +291,9 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Long Date" => "Data Por Extenso",
|
||||
"More Number Formats…" => "Mais Formatos de Número…",
|
||||
"Financial" => "Financeira",
|
||||
"Recently Used" => "Usadas Recentemente",
|
||||
"Logical" => "Lógica",
|
||||
"Date &\nTime" => "Data e\nHora",
|
||||
"Lookup &\nReference" => "Pesquisa e\nReferência",
|
||||
"Math &\nTrig" => "Matemática e\nTrigonometria",
|
||||
"More\nFunctions" => "Mais\nFunções",
|
||||
"More Functions" => "Mais Funções",
|
||||
"Page Layout|Orientation" => "Orientação",
|
||||
"Display theme" => "Tema de exibição",
|
||||
"Animations" => "Animações",
|
||||
|
||||
@@ -294,10 +294,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"More Number Formats…" => "Mai multe formate de număr…",
|
||||
"Financial" => "Financiare",
|
||||
"Logical" => "Logice",
|
||||
"Date &\nTime" => "Dată și\noră",
|
||||
"Lookup &\nReference" => "Căutare și\nreferință",
|
||||
"Math &\nTrig" => "Matematică și\ntrigonometrie",
|
||||
"More\nFunctions" => "Mai multe\nfuncții",
|
||||
"More Functions" => "Mai multe funcții",
|
||||
"Page Layout|Orientation" => "Orientare",
|
||||
"Display theme" => "Temă de afișare",
|
||||
"Animations" => "Animații",
|
||||
|
||||
@@ -295,11 +295,12 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Long Date" => "Длинная дата",
|
||||
"More Number Formats…" => "Другие числовые форматы…",
|
||||
"Financial" => "Финансовые",
|
||||
"Recently Used" => "Последние",
|
||||
"Logical" => "Логические",
|
||||
"Date &\nTime" => "Дата и\nвремя",
|
||||
"Lookup &\nReference" => "Поиск и\nссылки",
|
||||
"Math &\nTrig" => "Математические и\nтригонометрические",
|
||||
"More\nFunctions" => "Другие\nфункции",
|
||||
"Date & Time" => "Дата и время",
|
||||
"Lookup & Reference" => "Поиск и ссылки",
|
||||
"Math & Trig" => "Математические и тригонометрические",
|
||||
"More Functions" => "Другие функции",
|
||||
"Page Layout|Orientation" => "Ориентация",
|
||||
"Display theme" => "Тема оформления",
|
||||
"Animations" => "Анимация",
|
||||
|
||||
@@ -299,10 +299,10 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"More Number Formats…" => "Još formata brojeva…",
|
||||
"Financial" => "Finansijsko",
|
||||
"Logical" => "Logičko",
|
||||
"Date &\nTime" => "Datum i\nvreme",
|
||||
"Lookup &\nReference" => "Pretraga i\nreference",
|
||||
"Math &\nTrig" => "Matematika i\ntrigonometrija",
|
||||
"More\nFunctions" => "Još\nfunkcija",
|
||||
"Date & Time" => "Datum i vreme",
|
||||
"Lookup & Reference" => "Pretraga i reference",
|
||||
"Math & Trig" => "Matematika i trigonometrija",
|
||||
"More Functions" => "Još funkcija",
|
||||
"Sheet Options|View" => "Prikaz",
|
||||
"Print" => "Štampaj",
|
||||
"Print What" => "Šta štampati",
|
||||
|
||||
@@ -403,10 +403,10 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"More Number Formats…" => "Diğer Sayı Biçimleri…",
|
||||
"Financial" => "Finansal",
|
||||
"Logical" => "Mantıksal",
|
||||
"Date &\nTime" => "Tarih ve\nSaat",
|
||||
"Lookup &\nReference" => "Arama ve\nBaşvuru",
|
||||
"Math &\nTrig" => "Matematik ve\nTrigonometri",
|
||||
"More\nFunctions" => "Diğer\nİşlevler",
|
||||
"Date & Time" => "Tarih ve Saat",
|
||||
"Lookup & Reference" => "Arama ve Başvuru",
|
||||
"Math & Trig" => "Matematik ve Trigonometri",
|
||||
"More Functions" => "Diğer İşlevler",
|
||||
"Page Layout|Orientation" => "Sayfa Yönü",
|
||||
"Display theme" => "Görüntü teması",
|
||||
"System" => "Sistem",
|
||||
|
||||
@@ -289,10 +289,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"More Number Formats…" => "Інші числові формати…",
|
||||
"Financial" => "Фінансові",
|
||||
"Logical" => "Логічні",
|
||||
"Date &\nTime" => "Дата і\nчас",
|
||||
"Lookup &\nReference" => "Пошук і\nпосилання",
|
||||
"Math &\nTrig" => "Математичні і\nтригонометричні",
|
||||
"More\nFunctions" => "Інші\nфункції",
|
||||
"More Functions" => "Інші функції",
|
||||
"Page Layout|Orientation" => "Орієнтація",
|
||||
"Display theme" => "Тема оформлення",
|
||||
"Animations" => "Анімація",
|
||||
@@ -711,9 +708,9 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Hidden" => "Hidden",
|
||||
"Search:" => "Search:",
|
||||
"e.g. lookup, average, date" => "e.g. lookup, average, date",
|
||||
"Date & Time" => "Date & Time",
|
||||
"Lookup & Reference" => "Lookup & Reference",
|
||||
"Math & Trig" => "Math & Trig",
|
||||
"Date & Time" => "Дата і час",
|
||||
"Lookup & Reference" => "Пошук і посилання",
|
||||
"Math & Trig" => "Математичні і тригонометричні",
|
||||
"Statistical" => "Statistical",
|
||||
"Engineering" => "Engineering",
|
||||
"Database" => "Database",
|
||||
|
||||
@@ -303,10 +303,10 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"More Number Formats…" => "Định dạng số khác…",
|
||||
"Financial" => "Tài chính",
|
||||
"Logical" => "Logic",
|
||||
"Date &\nTime" => "Ngày &\nGiờ",
|
||||
"Lookup &\nReference" => "Tra cứu &\nTham chiếu",
|
||||
"Math &\nTrig" => "Toán &\nLượng giác",
|
||||
"More\nFunctions" => "Hàm\nkhác",
|
||||
"Date & Time" => "Ngày & Giờ",
|
||||
"Lookup & Reference" => "Tra cứu & Tham chiếu",
|
||||
"Math & Trig" => "Toán & Lượng giác",
|
||||
"More Functions" => "Hàm khác",
|
||||
"Sheet Options|View" => "Xem",
|
||||
"Print" => "In",
|
||||
"Print What" => "Nội dung in",
|
||||
|
||||
@@ -295,11 +295,12 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
||||
"Long Date" => "长日期",
|
||||
"More Number Formats…" => "更多数字格式…",
|
||||
"Financial" => "财务",
|
||||
"Recently Used" => "最近使用的函数",
|
||||
"Logical" => "逻辑",
|
||||
"Date &\nTime" => "日期和\n时间",
|
||||
"Lookup &\nReference" => "查找和\n引用",
|
||||
"Math &\nTrig" => "数学和\n三角函数",
|
||||
"More\nFunctions" => "更多\n函数",
|
||||
"Date & Time" => "日期和时间",
|
||||
"Lookup & Reference" => "查找和引用",
|
||||
"Math & Trig" => "数学和三角函数",
|
||||
"More Functions" => "更多函数",
|
||||
"Page Layout|Orientation" => "纸张方向",
|
||||
"Display theme" => "显示主题",
|
||||
"Animations" => "动画",
|
||||
|
||||
@@ -8,6 +8,27 @@ use egui::{Color32, Painter, Pos2, Rect, Stroke, pos2, vec2};
|
||||
|
||||
use crate::theme::{Cat, Tokens};
|
||||
|
||||
/// The glyph on a Function Library book ([`Icon::FnBook`]).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum FnGlyph {
|
||||
/// Recently Used: a star.
|
||||
Star,
|
||||
/// Financial: a stack of coins.
|
||||
Coins,
|
||||
/// Logical: a question mark.
|
||||
Question,
|
||||
/// Text: a letter.
|
||||
Letter,
|
||||
/// Date & Time: a clock face.
|
||||
Clock,
|
||||
/// Lookup & Reference: a magnifier.
|
||||
Magnifier,
|
||||
/// Math & Trig: theta.
|
||||
Theta,
|
||||
/// More Functions: three dots.
|
||||
Dots,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Icon {
|
||||
Paste,
|
||||
@@ -121,6 +142,9 @@ pub enum Icon {
|
||||
SheetList,
|
||||
Book,
|
||||
Folder,
|
||||
/// A book with a coloured spine and a category glyph (Formulas › Function Library). The
|
||||
/// spine and glyph take the accent colour of [`paint_accent`].
|
||||
FnBook(FnGlyph),
|
||||
/// Header & Footer: a page with its top / bottom band marked.
|
||||
Header,
|
||||
Footer,
|
||||
@@ -170,6 +194,10 @@ impl G<'_> {
|
||||
let v: Vec<Pos2> = pts.iter().map(|(x, y)| self.pt(*x, *y)).collect();
|
||||
self.p.add(PathShape::line(v, PathStroke::new(w * self.s.clamp(1.0, 2.0), c)));
|
||||
}
|
||||
fn closed(&self, pts: &[(f32, f32)], c: Color32, w: f32) {
|
||||
let v: Vec<Pos2> = pts.iter().map(|(x, y)| self.pt(*x, *y)).collect();
|
||||
self.p.add(PathShape::closed_line(v, PathStroke::new(w * self.s.clamp(1.0, 2.0), c)));
|
||||
}
|
||||
fn stroke(&self, pts: &[(f32, f32)], c: Color32) {
|
||||
self.line(pts, c, 1.15);
|
||||
}
|
||||
@@ -208,17 +236,20 @@ impl G<'_> {
|
||||
}
|
||||
|
||||
/// The colours an icon draws with besides its main colour: the paper inside documents and cells,
|
||||
/// and the category palette for its coloured parts.
|
||||
/// the category palette for its coloured parts, and the accent a button asked for.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
struct Palette {
|
||||
paper: Color32,
|
||||
cat: [Color32; 8],
|
||||
recording: Color32,
|
||||
/// The accent colour: the coloured part of an icon that has one (a book's spine and glyph),
|
||||
/// otherwise the icon's main colour. `None` draws the icon as it is.
|
||||
accent: Option<Color32>,
|
||||
}
|
||||
|
||||
impl Palette {
|
||||
fn of(t: &Tokens) -> Palette {
|
||||
Palette { paper: t.paper, cat: t.cat, recording: t.recording }
|
||||
Palette { paper: t.paper, cat: t.cat, recording: t.recording, accent: None }
|
||||
}
|
||||
|
||||
fn cat(&self, c: Cat) -> Color32 {
|
||||
@@ -242,11 +273,23 @@ pub fn fallback_for_symbol(c: char) -> Option<Icon> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether an icon has a part of its own for the accent colour (otherwise the accent recolours
|
||||
/// the whole icon).
|
||||
fn has_accent_part(icon: Icon) -> bool {
|
||||
matches!(icon, Icon::FnBook(_))
|
||||
}
|
||||
|
||||
/// Draws `icon` into `rect` (square-ish), in colour `c`, with the current theme's palette.
|
||||
pub fn paint(p: &Painter, rect: Rect, icon: Icon, c: Color32) {
|
||||
paint_with(p, rect, icon, c, &Palette::of(&Tokens::get(p.ctx())));
|
||||
}
|
||||
|
||||
/// Draws `icon` like [`paint`], with an accent colour: the icon's coloured part when it has one
|
||||
/// (the spine and glyph of [`Icon::FnBook`]), otherwise the whole icon. `None` is [`paint`].
|
||||
pub fn paint_accent(p: &Painter, rect: Rect, icon: Icon, c: Color32, accent: Option<Color32>) {
|
||||
paint_with(p, rect, icon, c, &Palette { accent, ..Palette::of(&Tokens::get(p.ctx())) });
|
||||
}
|
||||
|
||||
/// Draws `icon` on the sheet, which stays white in both themes, so with the light palette.
|
||||
pub fn paint_on_sheet(p: &Painter, rect: Rect, icon: Icon, c: Color32) {
|
||||
paint_with(p, rect, icon, c, &Palette::of(&Tokens::light()));
|
||||
@@ -257,6 +300,10 @@ fn paint_with(p: &Painter, rect: Rect, icon: Icon, c: Color32, pal: &Palette) {
|
||||
let side = rect.width().min(rect.height());
|
||||
let r = Rect::from_center_size(rect.center(), vec2(side, side));
|
||||
let g = G { p, r, s: side / 16.0 };
|
||||
let c = match pal.accent {
|
||||
Some(a) if !has_accent_part(icon) => a,
|
||||
_ => c,
|
||||
};
|
||||
let light = c.gamma_multiply(0.45);
|
||||
let paper = pal.paper;
|
||||
let blue = pal.cat(Cat::Blue);
|
||||
@@ -690,6 +737,46 @@ fn paint_with(p: &Painter, rect: Rect, icon: Icon, c: Color32, pal: &Palette) {
|
||||
Icon::Folder => {
|
||||
g.poly(&[(1.5, 4.0), (6.0, 4.0), (7.5, 5.5), (14.5, 5.5), (14.5, 13.0), (1.5, 13.0)], yellow);
|
||||
}
|
||||
Icon::FnBook(glyph) => {
|
||||
let spine = pal.accent.unwrap_or(c);
|
||||
g.rect(3.0, 1.5, 10.5, 13.0, Some(paper), Some(c));
|
||||
g.rect(3.0, 1.5, 2.5, 13.0, Some(spine), None);
|
||||
let (x, y) = (9.5, 7.8);
|
||||
match glyph {
|
||||
FnGlyph::Star => {
|
||||
let pts: Vec<(f32, f32)> = (0..10)
|
||||
.map(|i| {
|
||||
let a = std::f32::consts::PI * (i as f32 / 5.0 - 0.5);
|
||||
let rad = if i % 2 == 0 { 3.4 } else { 1.4 };
|
||||
(x + rad * a.cos(), y + rad * a.sin())
|
||||
})
|
||||
.collect();
|
||||
g.closed(&pts, spine, 1.0);
|
||||
}
|
||||
FnGlyph::Coins => {
|
||||
for k in 0..3 {
|
||||
let top = y + 1.6 - k as f32 * 2.3;
|
||||
g.p.rect_filled(Rect::from_min_max(g.pt(x - 2.8, top), g.pt(x + 2.8, top + 1.7)), 0.9 * g.s, spine);
|
||||
}
|
||||
}
|
||||
FnGlyph::Question => g.text(x, y, "?", 8.5, spine, (true, false)),
|
||||
FnGlyph::Letter => g.text(x, y, "A", 8.0, spine, (true, false)),
|
||||
FnGlyph::Clock => {
|
||||
g.circle(x, y, 3.2, None, Some(spine));
|
||||
g.stroke(&[(x, y - 2.0), (x, y), (x + 1.6, y + 0.9)], spine);
|
||||
}
|
||||
FnGlyph::Magnifier => {
|
||||
g.circle(x - 0.6, y - 0.6, 2.3, None, Some(spine));
|
||||
g.line(&[(x + 1.0, y + 1.0), (x + 3.0, y + 3.0)], spine, 1.5);
|
||||
}
|
||||
FnGlyph::Theta => g.text(x, y, "θ", 8.5, spine, (false, false)),
|
||||
FnGlyph::Dots => {
|
||||
for dx in [-2.4, 0.0, 2.4] {
|
||||
g.circle(x + dx, y, 0.9, Some(spine), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Icon::Header | Icon::Footer => {
|
||||
g.rect(3.5, 1.5, 9.0, 13.0, None, Some(c));
|
||||
let y = if icon == Icon::Header { 2.5 } else { 11.0 };
|
||||
|
||||
@@ -242,6 +242,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[
|
||||
("MO", Entry::Finish(Action::Control("formulas.lookup"))), // Excel: Lookup & Reference Menu; group level: containment(Lookup & Reference)
|
||||
("MP", Entry::Finish(Action::Command("formulas.tracePrecedents"))), // Excel: Trace Precedents
|
||||
("MQ", Entry::Finish(Action::Control("formulas.moreFunctions"))), // Excel: More Functions Menu; group level: containment(More Functions)
|
||||
("MR", Entry::Finish(Action::Control("formulas.recentlyUsed"))), // Excel: Recently Used Menu; opens the Recently Used menu
|
||||
("MS", Entry::Continue), // Excel: Use in Formula Menu; group level: containment(Use in Formula)
|
||||
("MSP", Entry::Finish(Action::Dialog("pasteName"))), // Excel: Paste Names
|
||||
("MT", Entry::Finish(Action::Control("formulas.text"))), // Excel: Text Menu; group level: containment(Text)
|
||||
|
||||
@@ -1658,6 +1658,13 @@
|
||||
"note": "group level: containment(More Functions)",
|
||||
"target": "formulas.moreFunctions"
|
||||
},
|
||||
{
|
||||
"seq": "MR",
|
||||
"kind": "control",
|
||||
"excel": "Recently Used Menu",
|
||||
"note": "opens the Recently Used menu",
|
||||
"target": "formulas.recentlyUsed"
|
||||
},
|
||||
{
|
||||
"seq": "MS",
|
||||
"kind": "continue",
|
||||
|
||||
@@ -32,6 +32,7 @@ mod floating_pictures;
|
||||
pub mod fonts;
|
||||
pub mod formula_bar;
|
||||
mod formula_locale;
|
||||
pub mod function_library;
|
||||
pub mod grid;
|
||||
pub mod i18n;
|
||||
pub mod icons;
|
||||
|
||||
@@ -291,3 +291,31 @@ fn contextual_tabs(app: &SheetApp) -> Vec<&'static str> {
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tooltip_tests {
|
||||
/// A ribbon tooltip names its control only; `widgets::tooltip_shortcut` adds the shortcut of
|
||||
/// the platform it runs on. A Command-key glyph written into the text would show on Windows
|
||||
/// and Linux, where the shortcut is a Ctrl or Alt one (AutoSum is Alt+=).
|
||||
#[test]
|
||||
fn tooltips_leave_the_shortcut_to_the_platform_lookup() {
|
||||
let sources = [
|
||||
include_str!("ribbon.rs"),
|
||||
include_str!("ribbon/automate.rs"),
|
||||
include_str!("ribbon/contextual.rs"),
|
||||
include_str!("ribbon/controls.rs"),
|
||||
include_str!("ribbon/data.rs"),
|
||||
include_str!("ribbon/formulas.rs"),
|
||||
include_str!("ribbon/galleries.rs"),
|
||||
include_str!("ribbon/home.rs"),
|
||||
include_str!("ribbon/insert.rs"),
|
||||
include_str!("ribbon/page_layout.rs"),
|
||||
include_str!("ribbon/review.rs"),
|
||||
include_str!("ribbon/shortcuts.rs"),
|
||||
include_str!("ribbon/title_bar.rs"),
|
||||
include_str!("ribbon/view.rs"),
|
||||
];
|
||||
let mac_only = sources.iter().flat_map(|s| s.lines()).filter(|l| l.contains('\u{2318}')).collect::<Vec<_>>();
|
||||
assert!(mac_only.is_empty(), "Command-key glyph in ribbon text: {mac_only:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use super::controls::{act, anchor_keytip, control_keytip, invoke, keytip, menu_items, popup_command, sep};
|
||||
use super::home::autosum_menu;
|
||||
use super::{Icon, SheetApp, Ui, big_button, big_split_button, json, keytip_table, small_button, small_split_button};
|
||||
use super::{Cat, Icon, SheetApp, Tokens, Ui, big_button, big_split_button, json, keytip_table, small_button, small_split_button};
|
||||
use crate::function_library;
|
||||
use crate::icons::FnGlyph;
|
||||
use crate::widgets::small_button_accent;
|
||||
|
||||
/// How many cells Formulas › Error Checking › Circular References lists.
|
||||
const CIRCULAR_MENU_LIMIT: usize = 100;
|
||||
@@ -19,6 +21,57 @@ fn circular_references(app: &SheetApp) -> Vec<(usize, String, String)> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// What a Function Library button's menu lists.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum LibraryMenu {
|
||||
Recent,
|
||||
Category(&'static str),
|
||||
More,
|
||||
}
|
||||
|
||||
/// One Function Library category button: its label, its menu, the glyph and colour of its book,
|
||||
/// and the control id its keytip sequence targets in `keytips.json`.
|
||||
struct LibraryItem {
|
||||
label: &'static str,
|
||||
menu: LibraryMenu,
|
||||
book: (FnGlyph, Cat),
|
||||
keytip: &'static str,
|
||||
}
|
||||
|
||||
impl LibraryItem {
|
||||
const fn new(label: &'static str, menu: LibraryMenu, book: (FnGlyph, Cat), keytip: &'static str) -> LibraryItem {
|
||||
LibraryItem { label, menu, book, keytip }
|
||||
}
|
||||
}
|
||||
|
||||
/// The Function Library categories after AutoSum, column by column (three rows each). Every
|
||||
/// category has its own colour.
|
||||
const LIBRARY: [LibraryItem; 8] = [
|
||||
LibraryItem::new("Recently Used", LibraryMenu::Recent, (FnGlyph::Star, Cat::Amber), "formulas.recentlyUsed"),
|
||||
LibraryItem::new("Financial", LibraryMenu::Category("Financial"), (FnGlyph::Coins, Cat::Green), "formulas.financial"),
|
||||
LibraryItem::new("Logical", LibraryMenu::Category("Logical"), (FnGlyph::Question, Cat::Purple), "formulas.logical"),
|
||||
LibraryItem::new("Text", LibraryMenu::Category("Text"), (FnGlyph::Letter, Cat::Blue), "formulas.text"),
|
||||
LibraryItem::new("Date & Time", LibraryMenu::Category("DateTime"), (FnGlyph::Clock, Cat::Orange), "formulas.dateTime"),
|
||||
LibraryItem::new("Lookup & Reference", LibraryMenu::Category("Lookup"), (FnGlyph::Magnifier, Cat::Teal), "formulas.lookup"),
|
||||
LibraryItem::new("Math & Trig", LibraryMenu::Category("MathTrig"), (FnGlyph::Theta, Cat::Red), "formulas.mathTrig"),
|
||||
LibraryItem::new("More Functions", LibraryMenu::More, (FnGlyph::Dots, Cat::Slate), "formulas.moreFunctions"),
|
||||
];
|
||||
|
||||
/// One Function Library category button: a book in the category's colour that opens the
|
||||
/// category's menu.
|
||||
fn library_button(app: &mut SheetApp, ui: &mut Ui, item: &LibraryItem) {
|
||||
let (glyph, cat) = item.book;
|
||||
let accent = Some(Tokens::get(ui.ctx()).cat(cat));
|
||||
let anchor = small_button_accent(ui, Icon::FnBook(glyph), accent, item.label, item.label, true);
|
||||
control_keytip(app, ui, &anchor, item.keytip);
|
||||
let forced = invoke(app, item.keytip);
|
||||
egui::Popup::menu(&anchor).open_memory(popup_command(forced, anchor.clicked())).show(|ui| match item.menu {
|
||||
LibraryMenu::Recent => function_library::recent_menu(app, ui),
|
||||
LibraryMenu::Category(id) => function_library::category_menu(app, ui, id),
|
||||
LibraryMenu::More => function_library::more_menu(app, ui),
|
||||
});
|
||||
}
|
||||
|
||||
/// The Formulas tab: Insert Function, AutoSum, the function-category menus (names in the formula
|
||||
/// language, descriptions in the interface language), defined names, auditing and calculation.
|
||||
pub(super) fn formulas(app: &mut SheetApp, ui: &mut Ui) {
|
||||
@@ -27,42 +80,21 @@ pub(super) fn formulas(app: &mut SheetApp, ui: &mut Ui) {
|
||||
if __k.clicked() {
|
||||
app.open_dialog("insertFunction", json!({}));
|
||||
}
|
||||
let (s, sa) = big_split_button(ui, Icon::Sum, "AutoSum", "AutoSum");
|
||||
control_keytip(app, ui, &sa, "formulas.autoSum");
|
||||
if s.clicked() {
|
||||
// Function Library: AutoSum as a big split button, then the categories in columns of three
|
||||
// labelled small buttons, as Excel lays the group out when the window is not wide enough for
|
||||
// a big button per category.
|
||||
let (sum, sum_arrow) = big_split_button(ui, Icon::Sum, "AutoSum", "AutoSum");
|
||||
control_keytip(app, ui, &sum_arrow, "formulas.autoSum");
|
||||
if sum.clicked() {
|
||||
act(app, "formulas.autoSum", json!({}));
|
||||
}
|
||||
egui::Popup::menu(&sa).show(|ui| autosum_menu(app, ui));
|
||||
for (label, cat, cid) in [
|
||||
("Financial", "Financial", "formulas.financial"),
|
||||
("Logical", "Logical", "formulas.logical"),
|
||||
("Text", "Text", "formulas.text"),
|
||||
("Date &\nTime", "DateTime", "formulas.dateTime"),
|
||||
("Lookup &\nReference", "Lookup", "formulas.lookup"),
|
||||
("Math &\nTrig", "MathTrig", "formulas.mathTrig"),
|
||||
("More\nFunctions", "Statistical", "formulas.moreFunctions"),
|
||||
] {
|
||||
let r = big_button(ui, Icon::Book, label, label, true);
|
||||
control_keytip(app, ui, &r, cid);
|
||||
let forced_c = invoke(app, cid);
|
||||
egui::Popup::menu(&r).open_memory(popup_command(forced_c, r.clicked())).show(|ui| {
|
||||
ui.set_max_height(420.0);
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
for f in gridcraft_engine::cmd::formulas::function_list() {
|
||||
if f["category"].as_str() == Some(cat)
|
||||
&& let Some(n) = f["name"].as_str()
|
||||
{
|
||||
let locale = app.ui.language.formula_locale();
|
||||
let name =
|
||||
app.session.active().map(|d| crate::formula_locale::completion_name(n, locale, &d.wb, d.wb.active_sheet())).unwrap_or(n);
|
||||
let desc = crate::formula_locale::description(n, locale, app.ui.language).unwrap_or_default();
|
||||
if ui.button(name).on_hover_text(desc).clicked() {
|
||||
app.run_or_alert("formulas.insertFunction", json!({"name": n}));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let forced = invoke(app, "formulas.autoSum");
|
||||
egui::Popup::menu(&sum_arrow).open_memory(popup_command(forced, sum_arrow.clicked())).show(|ui| function_library::auto_sum_menu(app, ui));
|
||||
for column in LIBRARY.chunks(3) {
|
||||
ui.vertical(|ui| {
|
||||
for item in column {
|
||||
library_button(app, ui, item);
|
||||
}
|
||||
});
|
||||
}
|
||||
sep(ui);
|
||||
@@ -200,3 +232,21 @@ pub(super) fn formulas(app: &mut SheetApp, ui: &mut Ui) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod function_library_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn every_function_library_book_has_its_own_glyph_and_colour() {
|
||||
let books: Vec<(FnGlyph, Cat)> = LIBRARY.iter().map(|i| i.book).collect();
|
||||
assert_eq!(books.len(), 8, "every category is a coloured book");
|
||||
for (i, (glyph, cat)) in books.iter().enumerate() {
|
||||
assert!(books.iter().skip(i + 1).all(|(g, c)| g != glyph && c != cat), "{glyph:?} / {cat:?} is used twice");
|
||||
}
|
||||
assert!(matches!(LIBRARY.first().map(|i| i.menu), Some(LibraryMenu::Recent)));
|
||||
for item in &LIBRARY {
|
||||
assert!(!keytip_table::sequences_for(item.keytip).is_empty(), "{} has a keytip sequence", item.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,7 +469,7 @@ pub(super) fn home(app: &mut SheetApp, ui: &mut Ui) {
|
||||
if s {
|
||||
act(app, "formulas.autoSum", json!({}));
|
||||
}
|
||||
egui::Popup::menu(&sa).show(|ui| autosum_menu(app, ui));
|
||||
egui::Popup::menu(&sa).show(|ui| crate::function_library::auto_sum_menu(app, ui));
|
||||
});
|
||||
let fill = small_button(ui, Icon::FillDown, "", "Fill", true);
|
||||
for sequence in keytip_table::group_extension(
|
||||
@@ -562,20 +562,6 @@ pub(super) fn home(app: &mut SheetApp, ui: &mut Ui) {
|
||||
});
|
||||
}
|
||||
|
||||
/// The AutoSum menu (Home › Editing and Formulas › Function Library): a function, or the Insert
|
||||
/// Function dialog.
|
||||
pub(super) fn autosum_menu(app: &mut SheetApp, ui: &mut Ui) {
|
||||
for f in ["SUM", "AVERAGE", "COUNT", "MAX", "MIN"] {
|
||||
if ui.button(f).clicked() {
|
||||
act(app, "formulas.autoSum", json!({"function": f}));
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("More Functions…").clicked() {
|
||||
app.open_dialog("insertFunction", json!({}));
|
||||
}
|
||||
}
|
||||
|
||||
fn font_combo(app: &mut SheetApp, ui: &mut Ui, st: &Style) {
|
||||
let name = st.font.name.clone();
|
||||
let resp = ui.add_sized([150.0, 24.0], egui::Button::new(egui::RichText::new(&name).font(theme::ui_font(12.5))));
|
||||
|
||||
@@ -183,6 +183,12 @@ fn paint_big_label(ui: &Ui, rect: Rect, lines: &[std::sync::Arc<egui::Galley>],
|
||||
|
||||
/// Small ribbon button: 16px icon with optional label to the right.
|
||||
pub fn small_button(ui: &mut Ui, icon: Icon, label: &str, tip: &str, dropdown: bool) -> Response {
|
||||
small_button_accent(ui, icon, None, label, tip, dropdown)
|
||||
}
|
||||
|
||||
/// Small ribbon button whose icon takes an accent colour (see [`icons::paint_accent`]); `None`
|
||||
/// draws it like [`small_button`]. A labelled button reports its label to accessibility.
|
||||
pub fn small_button_accent(ui: &mut Ui, icon: Icon, accent: Option<Color32>, label: &str, tip: &str, dropdown: bool) -> Response {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let font = theme::ui_font(12.5);
|
||||
let label = tr(ui, label);
|
||||
@@ -194,7 +200,7 @@ pub fn small_button(ui: &mut Ui, icon: Icon, label: &str, tip: &str, dropdown: b
|
||||
} else if resp.hovered() {
|
||||
ui.painter().rect_filled(rect, 4.0, t.hover);
|
||||
}
|
||||
icons::paint(ui.painter(), Rect::from_center_size(pos2(rect.left() + 12.0, rect.center().y), vec2(16.0, 16.0)), icon, t.text);
|
||||
icons::paint_accent(ui.painter(), Rect::from_center_size(pos2(rect.left() + 12.0, rect.center().y), vec2(16.0, 16.0)), icon, t.text, accent);
|
||||
if !label.is_empty() {
|
||||
ui.painter().text(pos2(rect.left() + 24.0, rect.center().y), Align2::LEFT_CENTER, label, font, t.text);
|
||||
}
|
||||
|
||||
@@ -152,8 +152,11 @@ fn right_clicking_a_chart_opens_its_menu_not_the_cells() {
|
||||
let (mut h, id) = fixture();
|
||||
let inside = corner(&h, id) - vec2(100.0, 80.0);
|
||||
right_click(&mut h, inside, vec2(0.0, 0.0));
|
||||
// Home › Cells › Delete has the same accessible name; the menu is drawn over the ribbon, last.
|
||||
h.get_all_by_role_and_label(Role::Button, "Delete").last().unwrap().click();
|
||||
// Home › Cells has a Delete button too: take the one in the chart's menu, by the click.
|
||||
h.query_all_by_label("Delete")
|
||||
.min_by(|a, b| a.rect().center().distance(inside).total_cmp(&b.rect().center().distance(inside)))
|
||||
.expect("the chart menu has Delete")
|
||||
.click();
|
||||
h.run_steps(3);
|
||||
assert!(h.state().session.doc().unwrap().wb.active().unwrap().charts.is_empty());
|
||||
assert!(h.state().grid.object_menu.is_none());
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
//! 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("));
|
||||
}
|
||||
@@ -1764,6 +1764,14 @@ fn keytip_mq_() {
|
||||
assert_eq!(h.state().keytips.prefix(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keytip_mr_() {
|
||||
let mut h = harness(blank());
|
||||
sequence(&mut h, "MR", false);
|
||||
assert!(h.state().message.is_none() && h.state().toast.is_none(), "MR must route to its control without an error box");
|
||||
assert_eq!(h.state().keytips.prefix(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keytip_ms_() {
|
||||
let mut h = harness(blank());
|
||||
@@ -2754,7 +2762,7 @@ fn keytip_q_() {
|
||||
|
||||
#[test]
|
||||
fn generated_test_count_matches_the_table() {
|
||||
const EXPECTED: usize = 365;
|
||||
const EXPECTED: usize = 366;
|
||||
assert_eq!(gridcraft_ui_egui::keytips::keytip_table::SEQUENCES.len(), EXPECTED);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ mod formula_locale;
|
||||
mod formula_point_headers;
|
||||
mod formula_popup;
|
||||
mod freeze_handles;
|
||||
mod function_library;
|
||||
mod go_to_dialog;
|
||||
mod header_autofit;
|
||||
mod header_column_drag;
|
||||
|
||||
@@ -135,15 +135,15 @@ fn big_split_button_separates_the_default_action_from_the_menu() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formulas_autosum_runs_on_the_icon_and_opens_its_menu_on_the_label() {
|
||||
fn formulas_autosum_runs_sum_and_its_arrow_opens_the_menu() {
|
||||
let mut h = app_at(1600.0, "Formulas");
|
||||
h.get_by_label("AutoSum").click();
|
||||
h.run_steps(2);
|
||||
assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.autoSum"), "the icon runs AutoSum");
|
||||
assert!(h.query_by_label("AVERAGE").is_none());
|
||||
assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.autoSum"), "the button runs AutoSum");
|
||||
assert!(h.query_by_label("Average").is_none());
|
||||
h.get_by_label("AutoSum options").click();
|
||||
h.run_steps(2);
|
||||
h.get_by_label("AVERAGE").click();
|
||||
h.get_by_label("Average").click();
|
||||
h.run_steps(2);
|
||||
let last = h.state().session.journal.iter().rev().find(|(id, _)| id == "formulas.autoSum").map(|(_, p)| p.clone());
|
||||
assert_eq!(last.and_then(|p| p.get("function").and_then(|f| f.as_str().map(str::to_string))).as_deref(), Some("AVERAGE"));
|
||||
|
||||
+13
-6
@@ -1,6 +1,6 @@
|
||||
# Architecture
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** trivial (the hidden names that hold macros; earlier: macro engine on the engine building blocks) · **Target:** Microsoft Excel (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (Function Library module; accent icons; earlier: the hidden names that hold macros) · **Target:** Microsoft Excel (Microsoft 365)
|
||||
|
||||
How GridCraft is built today. Rules for contributors are in [`AGENTS.md`](../AGENTS.md).
|
||||
|
||||
@@ -59,13 +59,19 @@ drawing its own:
|
||||
`icons::paint_on_sheet`) keep their light values. `theme::contrast_tests` holds every
|
||||
foreground/background pair to WCAG 2 (4.5:1 for text, 3:1 for glyphs) in both themes; a new
|
||||
token pair is added there. Icons (`icons.rs`, drawn in code) take their paper and category
|
||||
colours (`theme::Cat`) from the tokens.
|
||||
colours (`theme::Cat`) from the tokens; `icons::paint_accent` gives an icon an accent colour,
|
||||
drawn on its coloured part when it has one (the spine and glyph of `Icon::FnBook`).
|
||||
- **Ribbon widgets**: `widgets.rs` has the buttons every tab is built from (`big_button`,
|
||||
`small_button`, `split_button`, `big_split_button`, `toggle_button`), translated through
|
||||
`i18n` in one place. Split buttons return the main and arrow responses separately, with
|
||||
accessible names (`AutoSum`, `AutoSum options`). The ribbon card scrolls a tab wider than the
|
||||
window, with arrows at its edges; `ribbon::RibbonOverflow` records the groups' size each frame,
|
||||
`small_button`, `small_button_accent`, `split_button`, `big_split_button`, `toggle_button`),
|
||||
translated through `i18n` in one place. Split buttons return the main and arrow responses
|
||||
separately, with accessible names (`AutoSum`, `AutoSum options`); big and small buttons
|
||||
report their label (an icon-only one its tooltip) as their accessible name. The ribbon card
|
||||
scrolls a tab wider than the window, with arrows at its edges; `ribbon::RibbonOverflow` records the groups' size each frame,
|
||||
and a test keeps every tab within the card's three rows.
|
||||
- **Function Library**: `function_library.rs` holds the function categories (engine id and
|
||||
label, in the Insert Function dialog's order), the function list built once from
|
||||
`cmd::formulas::function_list`, and the menus of Formulas › Function Library, including the
|
||||
AutoSum menu that Home › Editing shares.
|
||||
- **Code editor**: `widgets::code_editor(ui, id, &mut text, Lang, max_lines)` (`code_editor.rs`)
|
||||
edits code in the monospace font with `Tokens::syntax` colours: Python (keywords, strings
|
||||
including triple-quoted ones, numbers, comments, builtins) and LaTeX (commands, scripts,
|
||||
@@ -159,6 +165,7 @@ rpm, tarball), FreeBSD, and web (WASM; WebGPU or WebGL2). Release workflows in
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-31 | minor | Function Library module (`function_library.rs`); accent icons (`icons::paint_accent`, `widgets::small_button_accent`) |
|
||||
| 2026-10-25 | trivial | The hidden names that hold macros are `_gc_macro_<n>_<k>` |
|
||||
| 2026-10-24 | minor | Macro engine on the building blocks (`macros/`, `cmd/macros.rs`) |
|
||||
| 2026-10-24 | minor | Engine building blocks for multi-command actions: undo groups, recording buffer, document trust, chunked hidden-name storage |
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
# UI parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (checkbox cells print as boxes; earlier: Ctrl+arrow and Ctrl+Shift+arrow while pointing a formula reference; earlier: editing a hyperlink loads its existing fields; earlier: AutoSum over a selected row puts its total to the right; ribbon catalog count 276 / 292) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (Formulas › Function Library as labelled buttons with category menus; earlier: checkbox cells print as boxes; Ctrl+arrow and Ctrl+Shift+arrow while pointing a formula reference; editing a hyperlink loads its existing fields; AutoSum over a selected row puts its total to the right; ribbon catalog count 276 / 292) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
|
||||
Interaction, controls and feel, against Excel. Part of [target-app-parity.md](target-app-parity.md).
|
||||
**UI/UX fidelity: ~55% ready, 50–80 h** (estimated). Ribbon command coverage is measured
|
||||
@@ -18,6 +18,7 @@ Sources: `crates/ui-egui/src` (ribbon, grid, editor, formula bar, dialogs, panes
|
||||
| Title bar, Quick Access Toolbar, AutoSave toggle | yes | yes | done; macOS window controls misaligned (#170), Dock icon size (#169) |
|
||||
| Ribbon: tabs, groups, split buttons, galleries, contextual tabs | yes | yes (Table Design, Chart Design, Format, PivotTable, Header & Footer tabs); Fill Color, Font Color and the Format Cells colour pickers end with Recent Colors and More Colors… (#928); large split buttons (Formulas › AutoSum); arrows at the card's edges scroll a tab wider than the window | done; missing contextual tabs for pictures, shapes, sparklines, slicers; groups do not collapse when narrow |
|
||||
| Ribbon collapse, simplified ribbon, customize ribbon/QAT | yes | partial | customization missing (File ▸ Options is the one missing File command) |
|
||||
| Formulas › Function Library | Insert Function, AutoSum, Recently Used and a menu per function category | yes (10-24): a big Insert Function button and three columns of labelled small buttons, the layout Excel uses when the window is not wide enough for a big button per category; each category's book icon has its own colour; a category menu shows each function's description on hover and ends with Insert Function…, which opens the dialog on that category; More Functions cascades into Statistical, Engineering, Cube, Information, Compatibility and Web; AutoSum is a split button whose menu (Sum, Average, Count Numbers, Max, Min, More Functions…) Home › Editing shares; Recently Used has the MR key tip | the wide layout with a big button per category is not offered; Recently Used lists what `formulas.recentlyUsed` returns |
|
||||
| Ribbon: tabs, groups, split buttons, galleries, contextual tabs | yes | yes (Table Design, Chart Design, Format, PivotTable tabs) | done; missing contextual tabs for pictures, shapes, sparklines, header/footer, slicers |
|
||||
| Ribbon collapse, simplified ribbon, customize ribbon/QAT | yes | partial | customization missing |
|
||||
| Key tips (Alt / F10 then letters) | yes, every ribbon control | **initial** (#43, 10-10): Alt or F10 shows key tips on Home for alignment, Paste Special and AutoFit plus legacy Alt sequences; other tabs and controls not yet (#33, #49) | 3–5 h |
|
||||
@@ -194,6 +195,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-31 | minor | Formulas › Function Library: Insert Function, the AutoSum split button and three columns of labelled small buttons with category colours; category menus end with Insert Function…; More Functions cascades; Home › Editing shares the AutoSum menu; MR key tip for Recently Used |
|
||||
| 2026-10-30 | minor | Checkbox cells print and export to PDF as boxes, as on screen: unchecked white with a grey outline, checked green with a white tick (#2972). They printed their value as TRUE or FALSE; other values in a checkbox cell still print as text |
|
||||
| 2026-10-29 | minor | Formula pointing from the keyboard: Ctrl+arrow (⌘arrow) moves the pointed cell to the edge of its data and Ctrl+Shift+arrow (⌘⇧arrow) extends the pointed range there, as the same keys move and extend the selection, so typing `=COUNTIF(`, clicking the top of a column and pressing ⌘⇧↓ points at the whole column of data; the keys used to move or extend by one cell. A range reaching the sheet edge is written as whole columns or rows (`A:B`). The editor reads the modifiers of the arrow's own key event, so a keystroke pressed and released within one frame (a control client's `ui.key`, an automation tool) keeps its Shift and Ctrl |
|
||||
| 2026-10-29 | minor | Insert Hyperlink opens with an existing link's address, display text and ScreenTip (#2876). An address-only edit preserves the cell's content and formula; Cancel leaves the link unchanged, and Undo restores the edit |
|
||||
|
||||
Reference in New Issue
Block a user