Merge PR #1056: Fix Command-click and Shift-click references during formula entry

contributors/people.toml: union of both new entries.
docs/ui-parity.md: one Last updated line, change notes combined; both history rows kept.
This commit is contained in:
aiongg
2026-10-22 02:36:14 +00:00
7 changed files with 182 additions and 17 deletions
+2 -1
View File
@@ -2,7 +2,7 @@
**Stage: alpha** · next: beta, ~25 points of "ready for real work" and ~160–240 h away **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-12 · **Change:** minor (rows auto-fit oversized fonts on open and whole-row/column formatting) · **Target:** Microsoft Excel (Microsoft 365) > **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-13 · **Change:** minor (formula-entry Command/Ctrl-click adds arguments and Shift-click extends references; readiness unchanged) · **Target:** Microsoft Excel (Microsoft 365)
GridCraft aims at full Microsoft Excel parity, and at being better in places: faster, open (XLSX 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. is the native format), fully scriptable by agents (MCP, control channel, CLI), and on the web.
@@ -132,6 +132,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
| Date | What landed | | Date | What landed |
|---|---| |---|---|
| 2026-10-13 | Formula-entry Command/Ctrl-click inserts another reference with the locale's argument separator; Shift-click extends from a stable, sheet-aware anchor. Pointer tests cover repeated clicks, reversed ranges, ordinary replacement, typed separators, Spanish syntax, sheet switches and keyboard continuation. Readiness estimates unchanged. |
| 2026-10-12 | Rows auto-fit oversized fonts: opening a file and whole-row/column formatting grow non-custom rows to their tallest font (#833) | | 2026-10-12 | Rows auto-fit oversized fonts: opening a file and whole-row/column formatting grow non-custom rows to their tallest font (#833) |
| 2026-10-12 | Number formats: parsing a format code is linear in its length (long runs of `[`, `,` or `/` no longer take quadratic time), and an unclosed `[` is a literal when splitting sections as well as when tokenizing (#810) | | 2026-10-12 | Number formats: parsing a format code is linear in its length (long runs of `[`, `,` or `/` no longer take quadratic time), and an unclosed `[` is a literal when splitting sections as well as when tokenizing (#810) |
| 2026-10-12 | Number formats: a fraction denominator with 65,536 zeros no longer crashes; format codes are capped at 255 characters like Excel (longer codes display as General, `TEXT` gives `#VALUE!`) (#807) | | 2026-10-12 | Number formats: a fraction denominator with 65,536 zeros no longer crashes; format codes are capped at 255 characters like Excel (longer codes display as General, `TEXT` gives `#VALUE!`) (#807) |
+3
View File
@@ -71,3 +71,6 @@ sync_github_name = true
real_name = "Ufuk Efe" real_name = "Ufuk Efe"
display_name = "Ufuk Efe" display_name = "Ufuk Efe"
sync_github_name = true sync_github_name = true
[people.pourie-he]
sync_github_name = true
+19 -3
View File
@@ -32,6 +32,8 @@ pub struct EditState {
pub point: Option<(usize, usize)>, pub point: Option<(usize, usize)>,
/// Where point mode's keyboard cursor is. /// Where point mode's keyboard cursor is.
pub point_cell: Option<CellRef>, pub point_cell: Option<CellRef>,
/// The fixed range anchor and its sheet while pointing with the mouse or keyboard.
pub point_anchor: Option<(usize, CellRef)>,
pub autocomplete: Vec<String>, pub autocomplete: Vec<String>,
pub ac_index: usize, pub ac_index: usize,
pub request_focus: bool, pub request_focus: bool,
@@ -56,6 +58,7 @@ impl EditState {
caret, caret,
point: None, point: None,
point_cell: None, point_cell: None,
point_anchor: None,
autocomplete: vec![], autocomplete: vec![],
ac_index: 0, ac_index: 0,
request_focus: true, request_focus: true,
@@ -81,9 +84,7 @@ impl EditState {
if !self.is_formula() { if !self.is_formula() {
return false; return false;
} }
if let Some((_, e)) = self.point if self.point_at_caret() {
&& e == self.byte_caret()
{
return true; return true;
} }
let before = self.text.get(..self.byte_caret()).unwrap_or(""); let before = self.text.get(..self.byte_caret()).unwrap_or("");
@@ -91,6 +92,10 @@ impl EditState {
matches!(last, Some('=' | '(' | ',' | '+' | '-' | '*' | '/' | '^' | '&' | '<' | '>' | ':' | ';' | '{')) matches!(last, Some('=' | '(' | ',' | '+' | '-' | '*' | '/' | '^' | '&' | '<' | '>' | ':' | ';' | '{'))
} }
pub fn point_at_caret(&self) -> bool {
self.point.is_some_and(|(start, end)| end == self.byte_caret() && self.text.get(start..end).is_some())
}
/// Inserts (or replaces the last pointed) reference text at the caret. /// Inserts (or replaces the last pointed) reference text at the caret.
pub fn insert_ref(&mut self, r: &str) { pub fn insert_ref(&mut self, r: &str) {
let caret_b = self.byte_caret(); let caret_b = self.byte_caret();
@@ -107,6 +112,17 @@ impl EditState {
self.sync_caret = true; self.sync_caret = true;
} }
/// Add another argument after the current pointed reference. Keep only the new reference
/// replaceable, so a subsequent plain click does not remove earlier arguments or the separator.
pub fn append_ref(&mut self, r: &str) {
let separator = if self.point_at_caret() { self.locale.list_separator().to_string() } else { String::new() };
self.point = None;
self.insert_ref(&format!("{separator}{r}"));
if let Some((start, end)) = self.point {
self.point = Some((start + separator.len(), end));
}
}
/// Updates function autocomplete for the word before the caret. /// Updates function autocomplete for the word before the caret.
pub fn update_autocomplete(&mut self, names: &[String]) { pub fn update_autocomplete(&mut self, names: &[String]) {
self.autocomplete.clear(); self.autocomplete.clear();
+1
View File
@@ -321,6 +321,7 @@ pub fn editor_widget(app: &mut SheetApp, ui: &mut egui::Ui, id: egui::Id, font:
ed.completion = crate::editor::column_completion(sh, ed.cell, &ed.text); ed.completion = crate::editor::column_completion(sh, ed.cell, &ed.text);
} }
ed.point = None; ed.point = None;
ed.point_anchor = None;
let names: Vec<String> = function_names() let names: Vec<String> = function_names()
.iter() .iter()
.map(|name| { .map(|name| {
+26 -10
View File
@@ -2095,14 +2095,26 @@ fn interact(app: &mut SheetApp, ui: &mut egui::Ui, resp: &egui::Response, geo: &
if let Some(ed) = app.editor.as_mut() if let Some(ed) = app.editor.as_mut()
&& ed.can_point() && ed.can_point()
{ {
let anchor = if mods.shift && ed.point_at_caret() {
ed.point_anchor.filter(|(sheet, _)| *sheet == wb.active_sheet).map(|(_, cell)| cell).unwrap_or(c)
} else {
c
};
let range = RangeRef::new(anchor, c);
let text = if range.is_single() { c.a1() } else { range.a1() };
let sheet_name = (ed.sheet != wb.active_sheet).then(|| sh.name.clone()); let sheet_name = (ed.sheet != wb.active_sheet).then(|| sh.name.clone());
let r = match sheet_name { let r = match sheet_name {
Some(n) => format!("{}!{}", gridcraft_engine::formula::quote_sheet(&n), c.a1()), Some(n) => format!("{}!{text}", gridcraft_engine::formula::quote_sheet(&n)),
None => c.a1(), None => text,
}; };
ed.insert_ref(&r); if mods.command && !mods.shift {
ed.append_ref(&r);
} else {
ed.insert_ref(&r);
}
ed.request_focus = true; ed.request_focus = true;
ed.point_cell = Some(c); ed.point_cell = Some(c);
ed.point_anchor = Some((wb.active_sheet, anchor));
app.grid.drag = Drag::Select; app.grid.drag = Drag::Select;
app.grid.drag_select = true; app.grid.drag_select = true;
if resp.drag_started() if resp.drag_started()
@@ -2364,6 +2376,7 @@ fn point_header(app: &mut SheetApp, wb: &Workbook, sh: &Sheet, range: RangeRef)
let r = range.a1(); let r = range.a1();
let r = if ed.sheet != wb.active_sheet { format!("{}!{r}", gridcraft_engine::formula::quote_sheet(&sh.name)) } else { r }; let r = if ed.sheet != wb.active_sheet { format!("{}!{r}", gridcraft_engine::formula::quote_sheet(&sh.name)) } else { r };
ed.insert_ref(&r); ed.insert_ref(&r);
ed.point_anchor = None;
ed.request_focus = true; ed.request_focus = true;
true true
} }
@@ -2376,12 +2389,13 @@ fn select_to_pointer(app: &mut SheetApp, geo: &Geo, sh: &Sheet, wb: &Workbook, p
if let Some(ed) = app.editor.as_mut() if let Some(ed) = app.editor.as_mut()
&& ed.point.is_some() && ed.point.is_some()
{ {
if let Some(start) = ed.point_cell { if let Some(start) = ed.point_anchor.filter(|(sheet, _)| *sheet == wb.active_sheet).map(|(_, cell)| cell).or(ed.point_cell) {
let range = RangeRef::new(start, cell); let range = RangeRef::new(start, cell);
let text = if range.is_single() { range.start.a1() } else { range.a1() }; let text = if range.is_single() { range.start.a1() } else { range.a1() };
let text = if ed.sheet != wb.active_sheet { format!("{}!{text}", gridcraft_engine::formula::quote_sheet(&sh.name)) } else { text }; let text = if ed.sheet != wb.active_sheet { format!("{}!{text}", gridcraft_engine::formula::quote_sheet(&sh.name)) } else { text };
ed.insert_ref(&text); ed.insert_ref(&text);
ed.point_cell = Some(start); ed.point_cell = Some(cell);
ed.point_anchor = Some((wb.active_sheet, start));
} }
} else if let Some(d) = app.session.active() { } else if let Some(d) = app.session.active() {
// Read the current anchor: starting a drag may have just replaced the selection. // Read the current anchor: starting a drag may have just replaced the selection.
@@ -2972,11 +2986,11 @@ pub fn point_move(app: &mut SheetApp, ed: &mut EditState, dr: i64, dc: i64, exte
let Some(d) = app.session.active() else { return }; let Some(d) = app.session.active() else { return };
let from = ed.point_cell.unwrap_or(ed.cell); let from = ed.point_cell.unwrap_or(ed.cell);
let to = from.offset_clamped(dr, dc); let to = from.offset_clamped(dr, dc);
let text = if extend { let anchor = if extend {
// The anchor is the corner opposite the moving end: the written range is normalized, so // The anchor is the corner opposite the moving end: the written range is normalized, so
// after extending up or left its first cell is the moving end, not the anchor. // after extending up or left its first cell is the moving end, not the anchor.
let pointed = ed.point.and_then(|(s, e)| ed.text.get(s..e)).and_then(RangeRef::parse); let pointed = ed.point.and_then(|(s, e)| ed.text.get(s..e)).and_then(RangeRef::parse);
let anchor = pointed let inferred_anchor = pointed
.map(|r| { .map(|r| {
CellRef::new( CellRef::new(
if from.row == r.start.row { r.end.row } else { r.start.row }, if from.row == r.start.row { r.end.row } else { r.start.row },
@@ -2984,13 +2998,15 @@ pub fn point_move(app: &mut SheetApp, ed: &mut EditState, dr: i64, dc: i64, exte
) )
}) })
.unwrap_or(from); .unwrap_or(from);
RangeRef::new(anchor, to).a1() ed.point_anchor.filter(|(sheet, _)| *sheet == d.wb.active_sheet).map(|(_, cell)| cell).unwrap_or(inferred_anchor)
} else { } else {
to.a1() to
}; };
let _ = d; let text = if extend { RangeRef::new(anchor, to).a1() } else { to.a1() };
let sheet = d.wb.active_sheet;
ed.insert_ref(&text); ed.insert_ref(&text);
ed.point_cell = Some(to); ed.point_cell = Some(to);
ed.point_anchor = Some((sheet, anchor));
app.grid.ensure_visible = true; app.grid.ensure_visible = true;
} }
+128 -1
View File
@@ -53,6 +53,16 @@ fn press(h: &mut Harness, cell: &str) {
frame(h, button(from, true)); frame(h, button(from, true));
} }
fn click_with_modifiers(h: &mut Harness, cell: &str, modifiers: Modifiers) {
let pos = point(h, cell);
frame(h, Event::ModifiersChanged(modifiers));
frame(h, Event::PointerMoved(pos));
for pressed in [true, false] {
frame(h, Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers });
}
h.run_steps(2);
}
fn assert_selection(h: &Harness, start: &str, end: &str) { fn assert_selection(h: &Harness, start: &str, end: &str) {
let selection = &h.state().session.active().unwrap().selection; let selection = &h.state().session.active().unwrap().selection;
let start = CellRef::parse(start).unwrap(); let start = CellRef::parse(start).unwrap();
@@ -134,7 +144,8 @@ fn formula_point_mode_consumes_first_movement_and_release() {
h.run_steps(1); h.run_steps(1);
let editor = h.state().editor.as_ref().unwrap(); let editor = h.state().editor.as_ref().unwrap();
assert_eq!(editor.text, "=C3:F7"); assert_eq!(editor.text, "=C3:F7");
assert_eq!(editor.point_cell, CellRef::parse("C3")); assert_eq!(editor.point_cell, CellRef::parse("F7"));
assert_eq!(editor.point_anchor, Some((0, CellRef::parse("C3").unwrap())));
assert_eq!(cells(&h), before); assert_eq!(cells(&h), before);
} }
@@ -159,3 +170,119 @@ fn formula_point_drag_keeps_the_sheet_qualifier() {
h.state_mut().session.execute("sheet.activate", json!({"sheet": "Sheet1"})).unwrap(); h.state_mut().session.execute("sheet.activate", json!({"sheet": "Sheet1"})).unwrap();
assert_eq!(cells(&h), before); assert_eq!(cells(&h), before);
} }
#[test]
fn formula_modifier_command_click_appends_an_argument() {
let mut h = harness("J10");
let before = cells(&h);
h.state_mut().begin_edit(Some("=SUM(".into()), false);
h.run_steps(2);
click_with_modifiers(&mut h, "C3", Modifiers::NONE);
let command = Modifiers { mac_cmd: true, command: true, ..Modifiers::NONE };
click_with_modifiers(&mut h, "F7", command);
assert_eq!(h.state().editor.as_ref().unwrap().text, "=SUM(C3,F7");
assert_eq!(cells(&h), before);
}
#[test]
fn formula_modifier_shift_click_extends_the_pointed_range() {
let mut h = harness("J10");
let before = cells(&h);
h.state_mut().begin_edit(Some("=SUM(".into()), false);
h.run_steps(2);
click_with_modifiers(&mut h, "C3", Modifiers::NONE);
click_with_modifiers(&mut h, "F7", Modifiers::SHIFT);
assert_eq!(h.state().editor.as_ref().unwrap().text, "=SUM(C3:F7");
assert_eq!(cells(&h), before);
}
#[test]
fn formula_modifier_command_click_keeps_previous_arguments_replaceable_independently() {
for command in [Modifiers { mac_cmd: true, command: true, ..Modifiers::NONE }, Modifiers { ctrl: true, command: true, ..Modifiers::NONE }] {
let mut h = harness("J10");
let before = cells(&h);
h.state_mut().begin_edit(Some("=SUM(".into()), false);
h.run_steps(2);
click_with_modifiers(&mut h, "C3", Modifiers::NONE);
click_with_modifiers(&mut h, "D4", command);
click_with_modifiers(&mut h, "F7", command);
assert_eq!(h.state().editor.as_ref().unwrap().text, "=SUM(C3,D4,F7");
click_with_modifiers(&mut h, "G7", Modifiers::NONE);
assert_eq!(h.state().editor.as_ref().unwrap().text, "=SUM(C3,D4,G7");
click_with_modifiers(&mut h, "F9", Modifiers::SHIFT);
assert_eq!(h.state().editor.as_ref().unwrap().text, "=SUM(C3,D4,F7:G9");
assert_eq!(cells(&h), before);
}
}
#[test]
fn formula_modifier_shift_click_keeps_its_anchor_in_both_directions() {
for (start, first, second, expected) in [("C3", "F7", "B2", "B2:C3"), ("F7", "C3", "H9", "F7:H9")] {
let mut h = harness("J10");
let before = cells(&h);
h.state_mut().begin_edit(Some("=SUM(".into()), false);
h.run_steps(2);
click_with_modifiers(&mut h, start, Modifiers::NONE);
click_with_modifiers(&mut h, first, Modifiers::SHIFT);
click_with_modifiers(&mut h, second, Modifiers::SHIFT);
assert_eq!(h.state().editor.as_ref().unwrap().text, format!("=SUM({expected}"));
assert_eq!(cells(&h), before);
}
}
#[test]
fn formula_modifier_command_click_respects_locale_and_a_typed_separator() {
use gridcraft_engine::formula::FormulaLocale;
let command = Modifiers { mac_cmd: true, command: true, ..Modifiers::NONE };
for (locale, prefix, separator) in [(FormulaLocale::En, "=SUM(", ","), (FormulaLocale::Es, "=SUMA(", ";")] {
let mut h = harness("J10");
h.state_mut().begin_edit(Some(prefix.into()), false);
h.state_mut().editor.as_mut().unwrap().locale = locale;
h.run_steps(2);
click_with_modifiers(&mut h, "C3", command);
assert_eq!(h.state().editor.as_ref().unwrap().text, format!("{prefix}C3"));
click_with_modifiers(&mut h, "F7", command);
assert_eq!(h.state().editor.as_ref().unwrap().text, format!("{prefix}C3{separator}F7"));
frame(&mut h, Event::ModifiersChanged(Modifiers::NONE));
frame(&mut h, Event::Text(separator.into()));
h.run_steps(2);
assert!(h.state().editor.as_ref().unwrap().point_anchor.is_none());
click_with_modifiers(&mut h, "G8", command);
assert_eq!(h.state().editor.as_ref().unwrap().text, format!("{prefix}C3{separator}F7{separator}G8"));
}
}
#[test]
fn formula_modifier_shift_click_does_not_reuse_an_anchor_from_another_sheet() {
let mut h = harness("J10");
h.state_mut().session.execute("home.insertSheet", json!({"name": "Other sheet"})).unwrap();
h.state_mut().session.execute("sheet.activate", json!({"sheet": "Sheet1"})).unwrap();
h.state_mut().begin_edit(Some("=SUM(".into()), false);
h.run_steps(2);
click_with_modifiers(&mut h, "C3", Modifiers::NONE);
h.state_mut().session.execute("sheet.activate", json!({"sheet": "Other sheet"})).unwrap();
h.run_steps(2);
click_with_modifiers(&mut h, "F7", Modifiers::SHIFT);
assert_eq!(h.state().editor.as_ref().unwrap().text, "=SUM('Other sheet'!F7");
click_with_modifiers(&mut h, "D4", Modifiers::SHIFT);
assert_eq!(h.state().editor.as_ref().unwrap().text, "=SUM('Other sheet'!D4:F7");
}
#[test]
fn formula_modifier_shift_click_then_arrow_moves_from_the_clicked_endpoint() {
use egui::Key;
for (modifiers, expected) in [(Modifiers::SHIFT, "=SUM(C3:G7"), (Modifiers::NONE, "=SUM(G7")] {
let mut h = harness("J10");
h.state_mut().begin_edit(Some("=SUM(".into()), false);
h.run_steps(2);
click_with_modifiers(&mut h, "C3", Modifiers::NONE);
click_with_modifiers(&mut h, "F7", Modifiers::SHIFT);
frame(&mut h, Event::ModifiersChanged(modifiers));
for pressed in [true, false] {
frame(&mut h, Event::Key { key: Key::ArrowRight, physical_key: None, pressed, repeat: false, modifiers });
}
h.run_steps(2);
assert_eq!(h.state().editor.as_ref().unwrap().text, expected);
assert_eq!(h.state().editor.as_ref().unwrap().point_cell, CellRef::parse("G7"));
}
}
+3 -2
View File
@@ -1,6 +1,6 @@
# UI parity # UI parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-19 · **Change:** minor (Ctrl+Enter fill gives each cell the automatic number format typing gives it; keytip letters typed with Alt held, #1846; Ctrl/Option-drag a sheet tab drops a copy, #971) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-19 · **Change:** minor (Ctrl+Enter fill gives each cell the automatic number format typing gives it; keytip letters typed with Alt held, #1846; Ctrl/Option-drag a sheet tab drops a copy, #971; formula-entry Command/Ctrl-click adds arguments and Shift-click extends a range) · **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). 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 **UI/UX fidelity: ~55% ready, 50–80 h** (estimated). Ribbon command coverage is measured
@@ -49,7 +49,7 @@ this logic, with one undo step.
| Fill handle: drag to fill series, double-click to fill down, right-drag menu | fill done (Ctrl+drag swaps copy and series); right-drag menu missing | | Fill handle: drag to fill series, double-click to fill down, right-drag menu | fill done (Ctrl+drag swaps copy and series); right-drag menu missing |
| Drag-move and drag-copy cells (border drag, Ctrl/Option, Shift to insert) | done (M3) | | 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 | | In-cell editing, Enter/Tab movement, Alt/Option+Enter line breaks | done |
| Point mode (click/drag/arrow references), reference colouring, F4 | done | | Point mode (click/drag/arrow references), reference colouring, F4 | done; Command/Ctrl-click adds separate arguments with the edit's locale separator, and Shift-click extends the current reference from a fixed anchor on the same sheet. Pointer-event regressions cover repeated/reversed selections, manual separators, sheet switches and keyboard continuation. |
| AutoComplete in columns, function autocomplete and argument tips | done; cell helpers clip to the worksheet and hide when the editor scrolls out (#200) | | AutoComplete in columns, function autocomplete and argument tips | done; cell helpers clip to the worksheet and hide when the editor scrolls out (#200) |
| Header resize and double-click AutoFit | done (#42, #88 for header dividers) | | Header resize and double-click AutoFit | done (#42, #88 for header dividers) |
| Outline symbols for grouped rows / columns (band beside the headers, brackets, − / +, level buttons 1…n+1) | done (#373): clicks run `data.hideDetail` / `data.showDetail` / `data.showLevels`, frozen panes and right-to-left sheets included; summary rows/columns are always below/right (the "Summary rows below detail" setting isn't modelled), no "Show outline symbols" option yet, and a block of cells still groups rows without the Rows / Columns dialog | | Outline symbols for grouped rows / columns (band beside the headers, brackets, − / +, level buttons 1…n+1) | done (#373): clicks run `data.hideDetail` / `data.showDetail` / `data.showLevels`, frozen panes and right-to-left sheets included; summary rows/columns are always below/right (the "Summary rows below detail" setting isn't modelled), no "Show outline symbols" option yet, and a block of cells still groups rows without the Rows / Columns dialog |
@@ -166,6 +166,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
| 2026-10-18 | minor | Keytip letters typed with left Alt held continue the sequence (Alt+H, O, A / Alt+H, O, I ran nothing before: Alt+letter cancelled keytips); other Alt combinations, AltGr and macOS Option are unchanged (#1846) | | 2026-10-18 | minor | Keytip letters typed with left Alt held continue the sequence (Alt+H, O, A / Alt+H, O, I ran nothing before: Alt+letter cancelled keytips); other Alt combinations, AltGr and macOS Option are unchanged (#1846) |
| 2026-10-12 | minor | The File view's recent list tells when each workbook was last changed, read off the UI thread | | 2026-10-12 | minor | The File view's recent list tells when each workbook was last changed, read off the UI thread |
| 2026-10-13 | minor | Horizontal scrollbar in the sheet tab bar: drag, track click pages, frozen columns excluded from the page (#840) | | 2026-10-13 | minor | Horizontal scrollbar in the sheet tab bar: drag, track click pages, frozen columns excluded from the page (#840) |
| 2026-10-13 | minor | Formula-entry Command/Ctrl-click adds arguments and Shift-click extends references; ordinary replacement and dragging retain their behavior. Readiness estimates unchanged. |
| 2026-10-12 | minor | Ctrl+wheel zoom steps along a ladder of percentages about 20% apart that never skips 100%, gliding to each step with animations on, so a notch out undoes a notch in (it drifted: 100 → 122 → 101); Excel steps 15 points, see decisions.md | | 2026-10-12 | minor | Ctrl+wheel zoom steps along a ladder of percentages about 20% apart that never skips 100%, gliding to each step with animations on, so a notch out undoes a notch in (it drifted: 100 → 122 → 101); Excel steps 15 points, see decisions.md |
| 2026-10-12 | minor | Page Layout view: left/right/top/bottom margins are dragged on the rulers (#792) | | 2026-10-12 | minor | Page Layout view: left/right/top/bottom margins are dragged on the rulers (#792) |
| 2026-10-12 | minor | Alt+Down opens the active filter header's existing popup, with validation priority and mirrored anchoring on right-to-left sheets (part of #441) | | 2026-10-12 | minor | Alt+Down opens the active filter header's existing popup, with validation priority and mirrored anchoring on right-to-left sheets (part of #441) |