Options: highlight the active cell's row, as a global preference

On a wide table the eye loses its place following a row from the active
cell. File > Options > General gets "Highlight the active cell's row", off
by default: a tint across the whole row of the active cell, over the fills
and under the text, in Normal and Page Layout views. It is a preference for
every sheet and workbook, so nothing goes into the file, the workbook does
not become dirty and there is no undo step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daniele MoriandClaude Opus 5.5 committed 2026-11-03 13:59:41 +01:00
1 parent 92cdc2a975
commit 38a7067ca2
14 files changed
+208 -3

No files matched your search

+4
View File
@@ -191,6 +191,10 @@ impl SheetApp {
self.ui.autocomplete = p.get("on").and_then(Json::as_bool).unwrap_or(!self.ui.autocomplete);
Ok(json!({"on": self.ui.autocomplete}))
}
"options.highlightActiveRow" => {
self.ui.highlight_active_row = p.get("on").and_then(Json::as_bool).unwrap_or(!self.ui.highlight_active_row);
Ok(json!({"on": self.ui.highlight_active_row}))
}
"view.interfaceScale" => {
// `{"percent": 125}` or `{"percent": "auto"}`; out-of-range values are clamped.
let percent = match p.get("percent") {
+4
View File
@@ -14,6 +14,10 @@ pub(super) fn options(app: &mut SheetApp, ui: &mut Ui) {
if ui.checkbox(&mut formula_bar, lang.tr("Show formula bar")).changed() {
app.run_or_alert("view.formulaBar", json!({"on": formula_bar}));
}
let mut highlight_row = app.ui.highlight_active_row;
if ui.checkbox(&mut highlight_row, lang.tr("Highlight the active cell's row")).changed() {
app.run_or_alert("options.highlightActiveRow", json!({"on": highlight_row}));
}
ui.add_space(18.0);
section(ui, lang, "Options|Editing");
let mut autocomplete = app.ui.autocomplete;
+1
View File
@@ -269,6 +269,7 @@ pub fn handle(app: &mut SheetApp, ctx: &egui::Context, method: &str, p: &Json) -
("view.theme", "Display Theme", ""),
("view.animations", "Animations", "{on?: bool} (toggles without it) → {on}"),
("options.autoComplete", "Enable AutoComplete for cell values", "{on?: bool} (toggles without it) → {on}"),
("options.highlightActiveRow", "Highlight the active cell's row", "{on?: bool} (toggles without it) → {on}"),
("view.interfaceScale", "Interface Size", "{percent: 75–200 | \"auto\"} (100 = auto; clamped)"),
("ui.dialog", "Open Dialog", ""),
("ui.backstage", "File (Backstage view)", "{page?: home|new|open|info|saveAs|print|export|options|account|close}"),
+2
View File
@@ -22,6 +22,8 @@ type PagesCache = ((u64, u64, usize), Vec<(RangeRef, u32)>);
/// Cached Show Details cells, keyed by (doc uid, revision, sheet, pivot).
type DetailsCache = ((u64, u64, usize, usize), Option<std::collections::HashSet<(u32, u32)>>);
mod active_row;
pub use active_row::active_row_band;
mod cell_paint;
mod cursor;
mod editor;
+28
View File
@@ -0,0 +1,28 @@
//! "Highlight the active cell's row" (File ▸ Options ▸ General): a tint across the whole row of
//! the active cell, so the eye keeps its place in a wide table.
use super::geo::Geo;
use super::quadrant::QuadrantPaint;
use super::{Rect, Sheet, pos2};
/// The tinted band of `row` inside one quadrant (`rows` x `cols`): none when the row is outside
/// the quadrant or hidden.
pub fn active_row_band(geo: &Geo, sh: &Sheet, row: u32, rows: (u32, u32), cols: (u32, u32)) -> Option<Rect> {
let height = sh.row_height(row) * geo.z;
if row < rows.0 || row > rows.1 || height <= 0.0 {
return None;
}
let top = geo.y(sh, row);
let right = geo.x(sh, cols.1) + sh.col_width(cols.1) * geo.z;
Some(Rect::from_min_max(pos2(geo.x(sh, cols.0), top), pos2(right, top + height)))
}
/// Paints the band of one quadrant, when the preference is on. With `within`, only the part of it
/// inside that area (text running past its cell has just cleared it).
pub(super) fn paint_active_row(q: &QuadrantPaint<'_>, within: Option<Rect>) {
let Some(band) = q.active_row.and_then(|row| active_row_band(q.geo, q.sheet, row, q.rows, q.cols)) else { return };
let part = within.map_or(band, |area| band.intersect(area));
if part.is_positive() {
q.painter.rect_filled(part, 0.0, q.tokens.active_row);
}
}
+7
View File
@@ -1,3 +1,4 @@
use super::active_row::paint_active_row;
use super::cell_paint::{
Look, MergedArea, cell_text_y, center_across_end, col32, display_text, look, merged_edges, paint_border, paint_cf_icon, painted_halign,
rotated_text_pos, shrink_font, wrapped_cell_text_y,
@@ -102,6 +103,8 @@ pub(crate) struct QuadrantPaint<'a> {
pub(crate) cols: (u32, u32),
pub(crate) cf: &'a mut CfCache,
pub(crate) tokens: &'a Tokens,
/// The row to tint, when "Highlight the active cell's row" is on.
pub(crate) active_row: Option<u32>,
}
/// Paint a worksheet quadrant, including backgrounds, borders, and text.
@@ -118,6 +121,8 @@ pub(crate) fn paint_quadrant(mut quadrant: QuadrantPaint<'_>) {
let cells = visible_cells(&quadrant);
let looks = paint_backgrounds(&mut quadrant, &cells, &merges);
let outside = outside_text_cells(&mut quadrant, &merges);
// Over the fills and under the text and borders.
paint_active_row(&quadrant, None);
paint_cells(&mut quadrant, &looks, &outside, &merges);
paint_borders(&mut quadrant, &looks, &merges);
paint_sparklines(&quadrant, range);
@@ -478,9 +483,11 @@ fn paint_cell(q: &QuadrantPaint<'_>, painted: &(CellRef, Rect, Look, Option<CfLo
let extra_r = Rect::from_min_max(pos2(rect.right() - 1.0, inner.top()), inner.max);
if extra_l.width() > 0.0 {
p.rect_filled(extra_l, 0.0, t.grid_bg);
paint_active_row(q, Some(extra_l));
}
if extra_r.width() > 0.0 {
p.rect_filled(extra_r, 0.0, t.grid_bg);
paint_active_row(q, Some(extra_r));
}
}
}
+13 -1
View File
@@ -404,7 +404,19 @@ fn paint_panes(input: PanesInput<'_>, cf: &mut CfCache) -> Vec<PaneQuad> {
continue;
}
let p = painter.with_clip_rect(*clip);
paint_quadrant(QuadrantPaint { painter: &p, geo, workbook: wb, sheet_index: si, sheet: sh, rows: *rows, cols: *cols, cf, tokens: t });
let active_row = app.ui.highlight_active_row.then_some(sel.active.row);
paint_quadrant(QuadrantPaint {
painter: &p,
geo,
workbook: wb,
sheet_index: si,
sheet: sh,
rows: *rows,
cols: *cols,
cf,
tokens: t,
active_row,
});
paint_selection(SelectionPaintInput { painter: &p, geo, sheet: sh, selection: sel, outline, tokens: t, app, rows: *rows, cols: *cols });
}
// Freeze lines.
+1
View File
@@ -410,6 +410,7 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
"Options|Save" => "Salvataggio",
"Options|Editing" => "Modifica",
"Show formula bar" => "Mostra barra della formula",
"Highlight the active cell's row" => "Evidenzia la riga della cella attiva",
"Enable AutoComplete for cell values" => "Abilita Completamento automatico per i valori delle celle",
"Save .xlsx workbooks automatically after every change" => "Salva automaticamente le cartelle di lavoro .xlsx dopo ogni modifica",
"Workbook calculation" => "Calcolo della cartella di lavoro",
+4
View File
@@ -94,6 +94,9 @@ pub struct UiState {
/// Enable AutoComplete for cell values (File ▸ Options ▸ Editing): typed text is completed
/// from the one matching entry in the same column.
pub autocomplete: bool,
/// Tint the active cell's whole row (File ▸ Options ▸ General), on every sheet and workbook, so
/// the eye keeps its place across a wide table. A preference: nothing goes into the file.
pub highlight_active_row: bool,
/// AutoSave (the title-bar switch): save saved-to-file .xlsx workbooks shortly after every
/// change. A preference like the rest of this struct — it stays on across restarts (#1901).
pub autosave: bool,
@@ -140,6 +143,7 @@ impl Default for UiState {
status_bar: true,
animations: true,
autocomplete: true,
highlight_active_row: false,
autosave: false,
recent: vec![],
keytip_invoke: None,
+13 -1
View File
@@ -162,7 +162,19 @@ pub(crate) fn paint_pages(input: PagePaintInput<'_>, cf: &mut CfCache) -> Vec<gr
}
painted += 1;
let pc = p.with_clip_rect(clip);
grid::paint_quadrant(grid::QuadrantPaint { painter: &pc, geo, workbook: wb, sheet_index: si, sheet: sh, rows, cols, cf, tokens: t });
let active_row = app.ui.highlight_active_row.then_some(sel.active.row);
grid::paint_quadrant(grid::QuadrantPaint {
painter: &pc,
geo,
workbook: wb,
sheet_index: si,
sheet: sh,
rows,
cols,
cf,
tokens: t,
active_row,
});
if !pg.is_data_page(i, j) {
// "Click to add data" pages: faded until something is typed there.
pc.rect_filled(clip, 0.0, t.grid_bg.gamma_multiply(0.75));
+3
View File
@@ -63,6 +63,8 @@ pub struct Tokens {
pub header_sel_text: Color32,
pub header_all_bg: Color32,
pub sel_fill: Color32,
/// The tint over the active cell's row (File ▸ Options ▸ General).
pub active_row: Color32,
pub sel_border: Color32,
pub input_bg: Color32,
pub input_border: Color32,
@@ -217,6 +219,7 @@ impl Tokens {
header_sel_text: pick(dark, Color32::from_rgb(0x0B, 0x5E, 0x31), Color32::from_rgb(0x6F, 0xD3, 0x9A)),
header_all_bg: pick(dark, Color32::from_rgb(0xC8, 0xE6, 0xD3), Color32::from_rgb(0x2E, 0x55, 0x3E)),
sel_fill: pick(dark, Color32::from_rgba_unmultiplied(0x10, 0x7C, 0x41, 0x1C), Color32::from_rgba_unmultiplied(0x10, 0x7C, 0x41, 0x24)),
active_row: pick(dark, Color32::from_rgba_unmultiplied(0x10, 0x7C, 0x41, 0x2C), Color32::from_rgba_unmultiplied(0x6F, 0xD3, 0x9A, 0x30)),
sel_border: Color32::from_rgb(0x10, 0x7C, 0x41),
input_bg: pick(dark, Color32::WHITE, Color32::from_rgb(0x1F, 0x1F, 0x1F)),
input_border: pick(dark, Color32::from_rgb(0xD1, 0xD1, 0xD1), Color32::from_rgb(0x50, 0x50, 0x50)),
@@ -0,0 +1,124 @@
//! "Highlight the active cell's row": a global preference, off by default, that tints the whole
//! row of the active cell and leaves the workbook alone.
use egui::{Shape, vec2};
use gridcraft_engine::Session;
use gridcraft_engine::core::CellRef;
use gridcraft_ui_egui::theme::Tokens;
use gridcraft_ui_egui::{SheetApp, UiState, grid::Geo, grid::active_row_band};
use serde_json::json;
fn harness(session: Session) -> egui_kittest::Harness<'static, SheetApp> {
let mut h = crate::common::builder().with_size(vec2(1200.0, 800.0)).build_ui_state(
|ui, app: &mut SheetApp| {
app.logic(ui.ctx());
app.ui(ui);
},
crate::common::app(session),
);
h.run_steps(4);
h
}
fn book(active: &str) -> Session {
let mut session = Session::new();
session.new_workbook();
session.execute("selection.set", json!({"range": active})).unwrap();
session
}
/// The rectangles painted with the highlight's own colour.
fn tinted(h: &egui_kittest::Harness<'_, SheetApp>) -> Vec<egui::Rect> {
let tint = Tokens::get(&h.ctx).active_row;
h.output()
.shapes
.iter()
.filter_map(|clipped| match &clipped.shape {
Shape::Rect(r) if r.fill == tint => Some(r.rect),
_ => None,
})
.collect()
}
#[test]
fn it_is_off_by_default_and_paints_nothing() {
assert!(!UiState::default().highlight_active_row);
let h = harness(book("C5"));
assert!(tinted(&h).is_empty());
}
#[test]
fn turned_on_it_tints_the_active_row_across_the_columns() {
let mut h = harness(book("C5"));
assert_eq!(h.state_mut().run("options.highlightActiveRow", json!({"on": true})), Ok(json!({"on": true})));
h.run_steps(2);
let bands = tinted(&h);
assert_eq!(bands.len(), 1, "{bands:?}");
let app = h.state();
let sheet = app.session.active().unwrap().wb.active().unwrap();
let geo = Geo::new(sheet, app.grid.rect.unwrap(), app.view().scroll);
let cell = geo.cell_rect(sheet, CellRef::parse("C5").unwrap());
assert_eq!((bands[0].top(), bands[0].bottom()), (cell.top(), cell.bottom()));
assert!(bands[0].left() <= geo.cells.left() + 1.0, "starts at the first column: {:?}", bands[0]);
assert!(bands[0].right() >= geo.cells.right() - 1.0, "reaches the last visible column: {:?}", bands[0]);
}
#[test]
fn the_band_follows_the_active_cell_and_only_the_row() {
let mut h = harness(book("C5"));
h.state_mut().run("options.highlightActiveRow", json!({"on": true})).unwrap();
h.state_mut().session.execute("selection.set", json!({"range": "E9"})).unwrap();
h.run_steps(2);
let bands = tinted(&h);
assert_eq!(bands.len(), 1, "a row, and no column band: {bands:?}");
let app = h.state();
let sheet = app.session.active().unwrap().wb.active().unwrap();
let geo = Geo::new(sheet, app.grid.rect.unwrap(), app.view().scroll);
assert_eq!(bands[0].top(), geo.cell_rect(sheet, CellRef::parse("E9").unwrap()).top());
}
#[test]
fn it_applies_to_every_sheet_and_workbook() {
let mut h = harness(book("C5"));
h.state_mut().run("options.highlightActiveRow", json!({"on": true})).unwrap();
h.state_mut().session.execute("home.insertSheet", json!({"name": "Other"})).unwrap();
h.run_steps(2);
assert_eq!(tinted(&h).len(), 1, "another sheet");
h.state_mut().session.new_workbook();
h.run_steps(2);
assert_eq!(tinted(&h).len(), 1, "another workbook");
}
#[test]
fn turning_it_off_clears_the_band_and_the_toggle_leaves_the_workbook_alone() {
let mut h = harness(book("C5"));
let dirty = h.state().session.active().unwrap().is_dirty();
assert_eq!(h.state_mut().run("options.highlightActiveRow", json!({})), Ok(json!({"on": true})), "no parameters: it toggles");
h.run_steps(2);
assert_eq!(h.state().session.active().unwrap().is_dirty(), dirty);
assert_eq!(h.state_mut().run("options.highlightActiveRow", json!({})), Ok(json!({"on": false})));
h.run_steps(2);
assert!(tinted(&h).is_empty());
assert_eq!(h.state().session.active().unwrap().is_dirty(), dirty);
}
#[test]
fn the_preference_survives_a_restart_and_older_settings_read_as_off() {
let state = UiState { highlight_active_row: true, ..UiState::default() };
let saved = serde_json::to_string(&state).unwrap();
assert!(serde_json::from_str::<UiState>(&saved).unwrap().highlight_active_row);
assert!(!serde_json::from_str::<UiState>("{}").unwrap().highlight_active_row);
}
#[test]
fn a_hidden_row_or_a_row_outside_the_quadrant_has_no_band() {
let mut session = book("A1");
session.execute("home.rowHeight", json!({"rows": "5:5", "height": 0})).unwrap();
let h = harness(session);
let app = h.state();
let sheet = app.session.active().unwrap().wb.active().unwrap();
let geo = Geo::new(sheet, app.grid.rect.unwrap(), app.view().scroll);
assert_eq!(active_row_band(&geo, sheet, 4, (0, 20), (0, 5)), None, "hidden");
assert_eq!(active_row_band(&geo, sheet, 30, (0, 20), (0, 5)), None, "outside");
assert!(active_row_band(&geo, sheet, 6, (0, 20), (0, 5)).is_some());
}
+1
View File
@@ -7,6 +7,7 @@
mod common;
mod accounting_fill;
mod active_row_highlight;
mod advanced_filter;
mod argument_names;
mod autosave_persist;
+3 -1
View File
@@ -1,6 +1,6 @@
# UI parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-02 · **Change:** minor (RefEdit reference boxes in dialogs: Goal Seek, Solver and Data Analysis use them, the arrow keys select while a dialog is collapsed and F4 cycles a reference's anchors; earlier: Python cells: Python mode in the formula bar; earlier: Insert › Equation and the equation editor; earlier: every floating object selects, resizes from eight handles and deletes with the keyboard; earlier: Formulas › Error Checking › Trace Error; trace arrows stay through a zoom (#3097); earlier: Page Setup sets the page order, Down, then over or Over, then down, which printing, the print preview, Page Break Preview and Page Layout view follow; earlier: Formulas › Function Library as labelled buttons with category menus; Recently Used and the Insert Function categories; Esc leaves a File-page text field before closing the view; 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
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (Highlight the active cell's row, a global preference in File › Options › General; earlier: RefEdit reference boxes in dialogs: Goal Seek, Solver and Data Analysis use them, the arrow keys select while a dialog is collapsed and F4 cycles a reference's anchors; earlier: Python cells: Python mode in the formula bar; earlier: Insert › Equation and the equation editor; earlier: every floating object selects, resizes from eight handles and deletes with the keyboard; earlier: Formulas › Error Checking › Trace Error; trace arrows stay through a zoom (#3097); earlier: Page Setup sets the page order, Down, then over or Over, then down, which printing, the print preview, Page Break Preview and Page Layout view follow; earlier: Formulas › Function Library as labelled buttons with category menus; Recently Used and the Insert Function categories; Esc leaves a File-page text field before closing the view; 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
@@ -54,6 +54,7 @@ this logic, with one undo step.
| Click/drag/Shift/Ctrl selection, multi-area selection | rectangular drag works in one direction only in some cases (#28) |
| Cursor while dragging cells, headers, freeze handles and objects | stable for the active drag (#936); Draw tools keep their crosshair; header arrows and a custom cell cursor remain unimplemented |
| Selection outline glides to a new selection (Excel: "Provide feedback with animation") | done (10-11): 0.12 s, decelerating, with the fill and fill handle; View ▸ Animations (`view.animations`) turns it off. The timing is an estimate, not yet measured against Excel; the system's reduced-motion setting isn't followed yet |
| Highlight the active cell's row (no Excel equivalent; Focus Cell tints the row and the column) | done (11-03): File › Options › General, off by default; a preference for every sheet and workbook, nothing saved in the file. Tints over the fills and under the text, in Normal and Page Layout views (`options.highlightActiveRow`) |
| Fill handle: drag to fill series, double-click to fill down, right-drag menu | fill done (Ctrl+drag swaps copy and series; month patterns use the workbook's 1900 or 1904 date system); right-drag menu partial: Copy Cells, Fill Series, Fill Formatting Only, Fill Without Formatting, and from a date Fill Days, Fill Weekdays, Fill Months and Fill Years, then Series…; a left-drag leaves an Auto Fill Options button with the same fill choices (gone once the workbook changes or the selection moves); Linear/Growth Trend and Flash Fill missing |
| Drag-move and drag-copy cells (border drag, Ctrl/Option, Shift to insert) | done (M3) |
| In-cell editing, Enter/Tab movement, Alt/Option+Enter line breaks | done |
@@ -237,6 +238,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
| Date | Change | Summary |
|---|---|---|
| 2026-11-03 | minor | Highlight the active cell's row: a global preference that tints the whole row, off by default |
| 2026-11-02 | minor | RefEdit reference boxes in dialogs: pick a range on the grid, collapse the dialog while selecting; Goal Seek, Solver (with the Model Range of Load/Save) and Data Analysis use them; while collapsed the arrow keys move the selection and Shift+arrow extends it, F4 cycles a reference's anchors, a click elsewhere keeps the box picking, a dialog opened over a picking one puts the selection back, OK stays on the sheet its command showed, and scripted cancel and confirm end a pick like the buttons; the dialog's sheet is followed through the recorded undo steps, so a sheet deleted and recreated under its name is not taken for it and an undo of the deletion brings it back; a box the dialog stops showing is no longer picked into |
| 2026-11-02 | minor | Python mode edits a cell's code in the formula bar's code editor (Ctrl+Alt+Shift+P, `=PY` then Tab, editing a Python cell), with the PY badge beside fx; its keys apply in input order, also several in one frame, and Ctrl+Enter commits through `python.set` (empty code clears the cell), keeping the code and showing the command's message when the code is refused |
| 2026-11-02 | minor | Insert › Symbols: Equation split button (access keys N E), a gallery of examples, and an equation editor with quick-insert buttons, the error underlined at its position and a live preview; double-clicking an equation edits it |