mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-24 10:46:50 -06:00
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:
+2
-1
@@ -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) |
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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) |
|
||||||
|
|||||||
Reference in New Issue
Block a user