Error Checking: its arrow opens a Circular References submenu listing a cell on each loop

Formulas > Error Checking is now a split button. The button opens the
Error Checking dialog as before, and its arrow opens a menu with Error
Checking… and Circular References. Circular References lists a cell on
each circular reference as Sheet!A1, up to 100, and picking one selects
it on its own sheet and scrolls the grid to it. With no circular
reference the item is unavailable.

The calculation kept only the loops its latest run found, so typing in
any cell outside a loop emptied the list, and the status bar's Circular
References with it, while the loop was still there. A partial
recalculation now keeps the loops among the formulas it does not
evaluate. In automatic calculation a loop leaves the list once an edit
breaks it. In manual calculation only the edited formulas run, so a
listed cell stays until it is edited or the workbook is recalculated.
A full recalculation or iterative calculation builds the list again.
This commit is contained in:
dotCooCoo
2026-10-24 14:28:19 -07:00
parent 3099df934e
commit 71669dae6d
25 changed files with 254 additions and 5 deletions
+13 -1
View File
@@ -1103,7 +1103,12 @@ impl Calc {
}
let tw = prof_now();
self.last_recalc_cells += results.len();
self.circular = cycles;
// The loops this round finds replace the ones listed for the formulas it evaluated.
if !self.circular.is_empty() {
let evaluated: HashSet<Key> = dirty.iter().copied().collect();
self.circular.retain(|k| !evaluated.contains(k));
}
self.circular.extend(cycles);
// Write back.
let mut spill_changes: Vec<Key> = Vec::new();
let mut freed: Vec<(usize, RangeRef)> = Vec::new();
@@ -1216,6 +1221,13 @@ impl Calc {
}
dirty = self.dirty_closure(wb, &spill_changes);
}
// Loops among formulas the run did not evaluate stay listed while they are still formulas.
// Iterative calculation reports none.
if wb.calc.iterative {
self.circular.clear();
} else {
self.circular.retain(|k| self.graph.nodes.contains_key(k));
}
}
}
+26
View File
@@ -384,6 +384,32 @@ fn circular_references_without_iterative_calculation() {
assert_eq!(t.get("A1"), Value::Error(CellError::Circ));
}
/// The list of circular references keeps the loops that a recalculation of other cells does not
/// reach, and drops a loop once a constant is typed over one of its cells.
#[test]
fn circular_references_stay_listed_until_the_loop_is_removed() {
let mut t = T::new();
t.set("A1", "=A1+1");
t.set("B1", "=B2");
t.set("B2", "=B1");
assert!(t.calc.circular.contains(&(0, c("A1"))), "{:?}", t.calc.circular);
t.set("D4", "7");
t.set("D5", "=D4*2");
assert!(t.calc.circular.contains(&(0, c("A1"))), "{:?}", t.calc.circular);
assert!(t.calc.circular.iter().any(|k| *k == (0, c("B1")) || *k == (0, c("B2"))), "{:?}", t.calc.circular);
t.set("A1", "5");
assert!(!t.calc.circular.contains(&(0, c("A1"))), "{:?}", t.calc.circular);
assert!(t.calc.circular.iter().any(|k| *k == (0, c("B1")) || *k == (0, c("B2"))), "{:?}", t.calc.circular);
t.set("B2", "3");
assert!(t.calc.circular.is_empty(), "{:?}", t.calc.circular);
// In manual calculation only the edited formulas are evaluated; a constant typed over the listed cell removes it.
t.wb.calc.mode = gridcraft_model::CalcMode::Manual;
t.set("C1", "=C1+1");
assert_eq!(t.calc.circular, vec![(0, c("C1"))]);
t.set("C1", "1");
assert!(t.calc.circular.is_empty(), "{:?}", t.calc.circular);
}
#[test]
fn date_text_coerces_in_the_workbook_date_system() {
for (sys, serial) in [(gridcraft_core::DateSystem::D1900, 1462.0), (gridcraft_core::DateSystem::D1904, 0.0)] {
+1
View File
@@ -142,6 +142,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
"Remove Arrows" => "Odebrat šipky",
"Show Formulas" => "Zobrazit vzorce",
"Error Checking" => "Kontrola chyb",
"Circular References" => "Cyklické odkazy",
"Evaluate Formula" => "Vyhodnotit vzorec",
"Watch Window" => "Okno sledování",
"Calculation\nOptions" => "Možnosti\nvýpočtu",
+1
View File
@@ -146,6 +146,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
"Remove Arrows" => "Pfeile entfernen",
"Show Formulas" => "Formeln anzeigen",
"Error Checking" => "Fehlerüberprüfung",
"Circular References" => "Zirkelbezüge",
"Evaluate Formula" => "Formel auswerten",
"Watch Window" => "Überwachungsfenster",
"Calculation\nOptions" => "Berechnungs-\noptionen",
+1
View File
@@ -141,6 +141,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
"Remove Arrows" => "Κατάργηση βελών",
"Show Formulas" => "Εμφάνιση τύπων",
"Error Checking" => "Έλεγχος σφαλμάτων",
"Circular References" => "Κυκλικές αναφορές",
"Evaluate Formula" => "Αξιολόγηση τύπου",
"Watch Window" => "Παράθυρο παρακολούθησης",
"Calculation\nOptions" => "Επιλογές\nυπολογισμού",
+1
View File
@@ -145,6 +145,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
"Remove Arrows" => "Quitar flechas",
"Show Formulas" => "Mostrar fórmulas",
"Error Checking" => "Comprobación de errores",
"Circular References" => "Referencias circulares",
"Evaluate Formula" => "Evaluar fórmula",
"Watch Window" => "Ventana Inspección",
"Calculation\nOptions" => "Opciones\nde cálculo",
+1
View File
@@ -154,6 +154,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
"Remove Arrows" => "Supprimer les flèches",
"Show Formulas" => "Afficher les formules",
"Error Checking" => "Vérification des erreurs",
"Circular References" => "Références circulaires",
"Evaluate Formula" => "Évaluation de formule",
"Watch Window" => "Fenêtre Espion",
"Calculation\nOptions" => "Options de\ncalcul",
+1
View File
@@ -149,6 +149,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
"Remove Arrows" => "Rimuovi frecce",
"Show Formulas" => "Mostra formule",
"Error Checking" => "Controllo errori",
"Circular References" => "Riferimenti circolari",
"Evaluate Formula" => "Valuta formula",
"Watch Window" => "Finestra controlli",
"Calculation\nOptions" => "Opzioni di\ncalcolo",
+1
View File
@@ -154,6 +154,7 @@ pub(super) const CORE: &[(&str, &str)] = &[
("Remove Arrows", "矢印の解除"),
("Show Formulas", "数式の表示"),
("Error Checking", "エラーチェック"),
("Circular References", "循環参照"),
("Evaluate Formula", "数式の検証"),
("Watch Window", "ウォッチ ウィンドウ"),
("Calculation\nOptions", "計算方法の設定"),
+1
View File
@@ -151,6 +151,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
"Remove Arrows" => "화살표 제거",
"Show Formulas" => "수식 표시",
"Error Checking" => "오류 검사",
"Circular References" => "순환 참조",
"Evaluate Formula" => "수식 계산",
"Watch Window" => "감시 창",
"Calculation\nOptions" => "계산\n옵션",
+1
View File
@@ -150,6 +150,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
"Remove Arrows" => "Pijlen verwijderen",
"Show Formulas" => "Formules weergeven",
"Error Checking" => "Foutopsporing",
"Circular References" => "Kringverwijzingen",
"Evaluate Formula" => "Formule evalueren",
"Watch Window" => "Bewakingsvenster",
"Calculation\nOptions" => "Berekenings-\nopties",
+1
View File
@@ -145,6 +145,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
"Remove Arrows" => "Usuń strzałki",
"Show Formulas" => "Pokaż formuły",
"Error Checking" => "Sprawdzanie błędów",
"Circular References" => "Odwołania cykliczne",
"Evaluate Formula" => "Szacuj formułę",
"Watch Window" => "Okno czujki",
"Calculation\nOptions" => "Opcje\nobliczania",
+1
View File
@@ -148,6 +148,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
"Remove Arrows" => "Remover Setas",
"Show Formulas" => "Exibir Fórmulas",
"Error Checking" => "Verificação de Erros",
"Circular References" => "Referências Circulares",
"Evaluate Formula" => "Avaliar Fórmula",
"Watch Window" => "Janela de Inspeção",
"Calculation\nOptions" => "Opções de\nCálculo",
+1
View File
@@ -152,6 +152,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
"Remove Arrows" => "Eliminare săgeți",
"Show Formulas" => "Afișare formule",
"Error Checking" => "Verificare erori",
"Circular References" => "Referințe circulare",
"Evaluate Formula" => "Evaluare formulă",
"Watch Window" => "Fereastră de urmărire",
"Calculation\nOptions" => "Opțiuni de\ncalcul",
+1
View File
@@ -151,6 +151,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
"Remove Arrows" => "Убрать стрелки",
"Show Formulas" => "Показать формулы",
"Error Checking" => "Проверка ошибок",
"Circular References" => "Циклические ссылки",
"Evaluate Formula" => "Вычислить формулу",
"Watch Window" => "Окно контрольных значений",
"Calculation\nOptions" => "Параметры\nвычислений",
+1
View File
@@ -146,6 +146,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
"Remove Arrows" => "Ukloni strelice",
"Show Formulas" => "Prikaži formule",
"Error Checking" => "Provera grešaka",
"Circular References" => "Kružne reference",
"Evaluate Formula" => "Proceni formulu",
"Watch Window" => "Prozor za praćenje",
"Calculation\nOptions" => "Opcije\nizračunavanja",
+1
View File
@@ -144,6 +144,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
"Remove Arrows" => "Okları Kaldır",
"Show Formulas" => "Formülleri Göster",
"Error Checking" => "Hata Denetimi",
"Circular References" => "Döngüsel Başvurular",
"Evaluate Formula" => "Formülü Değerlendir",
"Watch Window" => "Gözcü Penceresi",
"Calculation\nOptions" => "Hesaplama\nSeçenekleri",
+1
View File
@@ -147,6 +147,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
"Remove Arrows" => "Прибрати стрілки",
"Show Formulas" => "Показати формули",
"Error Checking" => "Перевірка помилок",
"Circular References" => "Циклічні посилання",
"Evaluate Formula" => "Обчислити формулу",
"Watch Window" => "Окно контрольних значень",
"Calculation\nOptions" => "Параметри\nобчислень",
+1
View File
@@ -153,6 +153,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
"Remove Arrows" => "Bỏ mũi tên",
"Show Formulas" => "Hiện công thức",
"Error Checking" => "Kiểm tra lỗi",
"Circular References" => "Tham chiếu vòng",
"Evaluate Formula" => "Đánh giá công thức",
"Watch Window" => "Cửa sổ theo dõi",
"Calculation\nOptions" => "Tùy chọn\ntính toán",
+1
View File
@@ -151,6 +151,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
"Remove Arrows" => "删除箭头",
"Show Formulas" => "显示公式",
"Error Checking" => "错误检查",
"Circular References" => "循环引用",
"Evaluate Formula" => "公式求值",
"Watch Window" => "监视窗口",
"Calculation\nOptions" => "计算\n选项",
+38 -4
View File
@@ -8,7 +8,7 @@ use crate::SheetApp;
use crate::icons::{self, Icon};
use crate::keytips::keytip_table;
use crate::theme::{self, Tokens};
use crate::widgets::{big_button, color_palette, icon_button, small_button, split_button, toggle_button};
use crate::widgets::{big_button, color_palette, icon_button, small_button, small_split_button, split_button, toggle_button};
pub const TABS: &[&str] = &["Home", "Insert", "Draw", "Page Layout", "Formulas", "Data", "Review", "View", "Automate"];
@@ -382,6 +382,23 @@ fn act(app: &mut SheetApp, id: &str, params: serde_json::Value) {
app.run_or_alert(id, params);
}
/// How many cells Formulas › Error Checking › Circular References lists.
const CIRCULAR_MENU_LIMIT: usize = 100;
/// The cell listed for each circular reference, for the Circular References submenu: each once, in the order
/// the calculation found them, as (sheet index, cell, "Sheet!A1").
fn circular_references(app: &SheetApp) -> Vec<(usize, String, String)> {
let Some(d) = app.session.active() else { return Vec::new() };
let mut seen = std::collections::HashSet::new();
d.calc
.circular
.iter()
.filter(|k| seen.insert(**k))
.filter_map(|(si, c)| d.wb.sheet(*si).map(|sh| (*si, c.a1(), format!("{}!{}", sh.name, c.a1()))))
.take(CIRCULAR_MENU_LIMIT)
.collect()
}
/// Keytips are painted, not focusable widgets: the grid retains keyboard focus.
fn keytip(app: &SheetApp, ui: &Ui, sequence: &str, at: egui::Pos2) {
let Some(rest) = app.keytips.prefix().and_then(|prefix| sequence.strip_prefix(prefix)).filter(|s| !s.is_empty()) else { return };
@@ -2009,11 +2026,28 @@ fn formulas(app: &mut SheetApp, ui: &mut Ui) {
if __k.clicked() {
act(app, "formulas.showFormulas", json!({}));
}
let __k = small_button(ui, Icon::Validation, "Error Checking", "Error Checking", false);
control_keytip(app, ui, &__k, "dialog:errorChecking");
if __k.clicked() {
let (ec, ec_arrow) = small_split_button(ui, Icon::Validation, "Error Checking", "Error Checking");
control_keytip(app, ui, &ec, "dialog:errorChecking");
if ec.clicked() {
app.open_dialog("errorChecking", json!({}));
}
egui::Popup::menu(&ec_arrow).show(|ui| {
let lang = app.ui.language;
if ui.button(format!("{}…", lang.tr("Error Checking"))).clicked() {
app.open_dialog("errorChecking", json!({}));
}
let circular = circular_references(app);
ui.add_enabled_ui(!circular.is_empty(), |ui| {
ui.menu_button(lang.tr("Circular References"), |ui| {
for (sheet, cell, label) in circular {
if ui.button(label).clicked() {
act(app, "selection.set", json!({"range": cell, "sheet": sheet}));
app.grid.ensure_visible = true;
}
}
});
});
});
let __k = small_button(ui, Icon::Calc, "Evaluate Formula", "Evaluate Formula", false);
control_keytip(app, ui, &__k, "dialog:evaluateFormula");
if __k.clicked() {
+28
View File
@@ -160,6 +160,34 @@ pub fn split_button(ui: &mut Ui, icon: Icon, accent: Option<Color32>, tip: &str)
(m.on_hover_text(tip_text(ui, tip)).clicked(), a)
}
/// A small ribbon split button with a label (Error Checking): the icon and label run the default
/// action, the arrow at the right opens a menu. Returns (main response, arrow response).
pub fn small_split_button(ui: &mut Ui, icon: Icon, label: &str, tip: &str) -> (Response, Response) {
let t = Tokens::get(ui.ctx());
let font = theme::ui_font(12.5);
let label = tr(ui, label);
let tw = ui.painter().layout_no_wrap(label.to_string(), font.clone(), t.text).size().x + 6.0;
let (rect, _) = ui.allocate_exact_size(vec2(24.0 + tw + 14.0, 23.0), Sense::hover());
let main = Rect::from_min_size(rect.min, vec2(24.0 + tw, rect.height()));
let arrow = Rect::from_min_max(pos2(main.right(), rect.top()), rect.max);
let m = ui.interact(main, ui.id().with((tip, "main")), Sense::click());
let a = ui.interact(arrow, ui.id().with((tip, "arrow")), Sense::click());
if m.hovered() || a.hovered() {
ui.painter().rect_filled(rect, 4.0, t.hover);
for (part, r) in [(&m, main), (&a, arrow)] {
if part.is_pointer_button_down_on() {
ui.painter().rect_filled(r, 4.0, t.pressed);
}
}
ui.painter().line_segment([pos2(main.right(), rect.top() + 3.0), pos2(main.right(), rect.bottom() - 3.0)], Stroke::new(1.0, t.separator));
}
icons::paint(ui.painter(), Rect::from_center_size(pos2(rect.left() + 12.0, rect.center().y), vec2(16.0, 16.0)), icon, t.text);
ui.painter().text(pos2(rect.left() + 24.0, rect.center().y), Align2::LEFT_CENTER, label, font, t.text);
icons::paint(ui.painter(), Rect::from_center_size(pos2(arrow.center().x, rect.center().y + 1.0), vec2(10.0, 10.0)), Icon::Chevron, t.text_dim);
let tip = tip_text(ui, tip);
(m.on_hover_text(tip.clone()), a.on_hover_text(tip))
}
/// A one-line text field for a value stored in the workbook, which callers reload every frame. The text being typed is
/// kept in egui memory until the field loses focus, then returned to commit (`None` when Escape cancels the edit).
pub fn committed_text(ui: &mut Ui, id: egui::Id, current: &str, width: Option<f32>) -> Option<String> {
@@ -0,0 +1,129 @@
//! Formulas › Error Checking is a split button, as in Excel: the button opens the Error Checking
//! dialog, and its arrow opens a menu whose Circular References submenu lists a cell on each
//! circular reference and selects the one picked, on its own sheet.
use egui::{Event, Shape, pos2, vec2};
use egui_kittest::kittest::{NodeT as _, Queryable as _};
use gridcraft_engine::Session;
use gridcraft_ui_egui::SheetApp;
use serde_json::json;
type Harness = egui_kittest::Harness<'static, SheetApp>;
/// The submenu item, with the arrow egui puts after a submenu's label.
const CIRCULAR: &str = "Circular References ⏵";
fn harness(loops: bool) -> Harness {
let mut s = Session::new();
s.new_workbook();
s.execute("home.insertSheet", json!({"name": "Data"})).unwrap();
if loops {
s.execute("cell.set", json!({"cell": "B3", "input": "=B4"})).unwrap();
s.execute("cell.set", json!({"cell": "B4", "input": "=B3"})).unwrap();
}
s.execute("sheet.activate", json!({"sheet": 0})).unwrap();
if loops {
s.execute("cell.set", json!({"cell": "C2", "input": "=C2+1"})).unwrap();
}
s.execute("selection.set", json!({"range": "H20"})).unwrap();
let mut h = egui_kittest::Harness::builder().with_size(vec2(1400.0, 800.0)).build_ui_state(
|ui, app: &mut SheetApp| {
app.logic(ui.ctx());
app.ui(ui);
},
SheetApp::new(s, Default::default()),
);
h.run_steps(4);
h.state_mut().ui.ribbon_tab = "Formulas".into();
h.run_steps(4);
h
}
/// The painted "Error Checking" ribbon label: its left edge, right edge and vertical center.
fn label(h: &Harness) -> (f32, f32, f32) {
h.output()
.shapes
.iter()
.find_map(|s| match &s.shape {
Shape::Text(t) if t.galley.job.text == "Error Checking" => {
Some((t.pos.x, t.pos.x + t.galley.size().x, t.pos.y + t.galley.size().y / 2.0))
}
_ => None,
})
.expect("the Formulas tab shows Error Checking")
}
fn click(h: &mut Harness, at: egui::Pos2) {
h.input_mut().events.push(Event::PointerMoved(at));
h.run_steps(2);
for pressed in [true, false] {
h.input_mut().events.push(Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE });
h.run_steps(2);
}
h.run_steps(2);
}
fn open_menu(h: &mut Harness) {
let (_, right, y) = label(h);
// The arrow sits right of the label, past the 6 px gap after the text.
click(h, pos2(right + 13.0, y));
}
fn hover(h: &mut Harness, label: &str) {
let at = h.get_by_label(label).rect().center();
h.input_mut().events.push(Event::PointerMoved(at));
h.run_steps(6);
}
#[test]
fn error_checking_button_opens_the_dialog_and_its_arrow_opens_the_menu() {
let mut h = harness(false);
let (left, _, y) = label(&h);
click(&mut h, pos2(left + 4.0, y));
assert_eq!(h.state().dialog.as_ref().map(|d| d.name.as_str()), Some("errorChecking"), "the button opens Error Checking");
h.state_mut().dialog = None;
h.run_steps(2);
open_menu(&mut h);
assert!(h.state().dialog.is_none(), "the arrow opens a menu, not the dialog");
h.get_by_label("Error Checking…").click();
h.run_steps(4);
assert_eq!(h.state().dialog.as_ref().map(|d| d.name.as_str()), Some("errorChecking"), "the menu's first item opens Error Checking");
}
#[test]
fn circular_references_is_unavailable_without_a_circular_reference() {
let mut h = harness(false);
open_menu(&mut h);
assert!(h.get_by_label(CIRCULAR).accesskit_node().is_disabled());
}
#[test]
fn circular_references_lists_the_loops_and_selects_the_cell_picked() {
let mut h = harness(true);
open_menu(&mut h);
assert!(!h.get_by_label(CIRCULAR).accesskit_node().is_disabled());
hover(&mut h, CIRCULAR);
assert!(h.query_by_label("Sheet1!C2").is_some(), "the loop on the active sheet is listed");
let other = ["Data!B3", "Data!B4"].into_iter().find(|l| h.query_by_label(l).is_some()).expect("the loop on Data is listed");
h.get_by_label(other).click();
h.run_steps(4);
let app = h.state();
let d = app.session.active().unwrap();
assert_eq!(d.wb.active_sheet, 1, "picking a cell on Data goes to Data");
assert_eq!(format!("Data!{}", d.selection.active.a1()), other);
}
#[test]
fn circular_references_scrolls_to_the_cell_picked() {
let mut h = harness(false);
h.state_mut().run("cell.set", json!({"cell": "C500", "input": "=C500+1"})).unwrap();
h.state_mut().run("selection.set", json!({"range": "H20"})).unwrap();
h.run_steps(4);
assert_eq!(h.state().view().scroll.y, 0.0, "setup: the view is at the top of the sheet");
open_menu(&mut h);
hover(&mut h, CIRCULAR);
h.get_by_label("Sheet1!C500").click();
h.run_steps(4);
assert_eq!(h.state().session.active().unwrap().selection.active.a1(), "C500");
assert!(h.state().view().scroll.y > 0.0, "the grid scrolls to the cell picked");
}
+1
View File
@@ -24,6 +24,7 @@ mod dialog_confirm;
mod dialog_focus;
mod dialog_move;
mod drag_cursor;
mod error_checking_menu;
mod file_menu;
mod fill_handle_double_click;
mod fill_handle_right_drag;
+1
View File
@@ -24,6 +24,7 @@ Sources: `crates/ui-egui/src` (ribbon, grid, editor, formula bar, dialogs, panes
| Formula bar with Name Box, expand, function hints | yes | yes | done |
| Sheet tabs: rename, reorder, colour, hide, scroll | yes | yes | Ctrl-drag (Cmd or Option on macOS) drops a copy (#971); active tab revealed on navigation and resize (#681); manual wheel/trackpad browsing preserved; rename focus bug on macOS (#145) |
| Formula bar with Name Box, expand, function hints | yes | yes | done; dynamic-array spill children show the source formula muted and read-only, using current spill ownership |
| Formulas › Error Checking arrow: Circular References | yes | yes: the button opens Error Checking and its arrow opens a menu with Error Checking… and Circular References, which lists a cell on each circular reference as `Sheet!A1`, up to 100, and selects the one picked, on its own sheet (unavailable when there are none) | done; the Error Checking… item and the limit of 100 are GridCraft's own |
| Sheet tabs: rename, reorder, colour, hide, scroll | yes | yes | active tab revealed on navigation and resize (#681); manual wheel/trackpad browsing preserved; rename focus bug on macOS (#145) |
| Sheet tabs: rename, reorder, colour, hide, scroll | yes | yes | active tab revealed on navigation and resize (#681); New sheet (+) right after the last tab, the strip capped at 60% of the bar (#943); manual wheel/trackpad browsing preserved; next/previous skips Hidden and VeryHidden sheets and retains the current sheet and selection when no visible destination exists; rename focus bug on macOS (#145) |
| Status bar: Sum/Count/Average and more, view buttons, zoom | yes | yes | customizable statistics partial |