Point mode: dragging over row or column headers extends the reference

A press on a header while a formula waits for a reference inserted C:C, but the
drag that followed did nothing, so dragging from column C to E left C:C. The drag
now follows the pointer, as it does over cells, in either direction and for rows
too, keeping the sheet qualifier, the earlier arguments of a Ctrl press and the
far end of a Shift press.

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:45:53 +01:00
1 parent 92cdc2a975
commit f4f69beec2
5 files changed
+155 -7

No files matched your search

+4
View File
@@ -101,6 +101,10 @@ pub enum Drag {
Select,
Rows(u32),
Cols(u32),
/// Point mode over the row headers: the formula's reference runs from this row to the pointer.
PointRows(u32),
/// Point mode over the column headers: the formula's reference runs from this column to the pointer.
PointCols(u32),
/// Reordering one contiguous whole-column selection by its header.
MoveCols {
source: RangeRef,
+1 -1
View File
@@ -48,7 +48,7 @@ fn drag_cursor(drag: &Drag, rtl: bool) -> Option<CursorIcon> {
match drag {
Drag::None => None,
Drag::Select => Some(CursorIcon::Cell),
Drag::Rows(_) | Drag::Cols(_) => Some(CursorIcon::Default),
Drag::Rows(_) | Drag::Cols(_) | Drag::PointRows(_) | Drag::PointCols(_) => Some(CursorIcon::Default),
Drag::Fill { .. } => Some(CursorIcon::Crosshair),
Drag::Move { .. } | Drag::MoveCols { .. } | Drag::Object { handle: None, .. } => Some(CursorIcon::Move),
Drag::ResizeCol { .. } => Some(CursorIcon::ResizeColumn),
+58 -4
View File
@@ -307,6 +307,7 @@ fn handle_column_header_press(app: &mut SheetApp, input: &InteractionInput<'_>,
return;
}
if point_header(app, wb, sh, RangeRef::cols(c, c), mods.shift, mods.command) {
point_header_first_move(app, input);
return;
}
// An armed Format Painter paints the columns dragged over instead of moving them.
@@ -337,6 +338,7 @@ fn handle_row_header_press(app: &mut SheetApp, input: &InteractionInput<'_>, p:
}
let r = geo.row_at(sh, p.y);
if point_header(app, wb, sh, RangeRef::rows(r, r), mods.shift, mods.command) {
point_header_first_move(app, input);
return;
}
let range = if mods.shift { RangeRef::rows(sel.anchor.row.min(r), sel.anchor.row.max(r)) } else { RangeRef::rows(r, r) };
@@ -612,6 +614,10 @@ fn handle_dragging(app: &mut SheetApp, input: &InteractionInput<'_>) {
app.session.run("selection.set", json!({"range": RangeRef::cols(anchor.min(c), anchor.max(c)).a1(), "active": sel.active.a1()}));
scroll_by(app, vec2(auto.x, 0.0));
}
drag @ (Drag::PointRows(_) | Drag::PointCols(_)) => {
point_header_to(app, geo, sh, wb, &drag, p);
scroll_by(app, if matches!(drag, Drag::PointRows(_)) { vec2(0.0, auto.y) } else { vec2(auto.x, 0.0) });
}
Drag::MoveCols { source, .. } => {
let col = geo.col_at(sh, p.x.clamp(geo.cells.left() + 1.0, geo.cells.right() - 1.0));
let midpoint = geo.x(sh, col) + sh.col_width(col) * geo.z * 0.5;
@@ -688,6 +694,14 @@ fn handle_release(app: &mut SheetApp, input: &InteractionInput<'_>) {
}
// Whole rows or columns clicked or dragged over by their headers.
Drag::Rows(_) | Drag::Cols(_) => format_painter_apply(app),
// The release may be the first frame to report the pointer's last position.
drag @ (Drag::PointRows(_) | Drag::PointCols(_)) => {
if resp.drag_stopped()
&& let Some(p) = pos
{
point_header_to(app, geo, sh, wb, &drag, p);
}
}
// Esc mid-drag lands here too (egui ends the drag on Esc): no menu, and the key is
// spent on the drag so the grid's Esc doesn't also drop the copy.
Drag::Fill { target: Some(t), menu: true } => {
@@ -751,11 +765,13 @@ fn format_painter_apply(app: &mut SheetApp) {
}
}
/// Point mode on a header: inserts a whole-row or whole-column reference instead of selecting.
/// Point mode on a header: inserts a whole-row or whole-column reference instead of selecting,
/// and dragging on from there extends it.
fn point_header(app: &mut SheetApp, wb: &Workbook, sh: &Sheet, range: RangeRef, extend: bool, add: bool) -> bool {
let Some(ed) = app.editor.as_mut().filter(|ed| ed.can_point()) else { return false };
let edit_sheet = ed.sheet;
let active_sheet = wb.active_sheet();
let pressed = range;
let range = if extend {
ed.point
.and_then(|point| crate::editor::formula_refs(&ed.text).into_iter().find(|(start, end, ..)| (*start, *end) == point))
@@ -776,9 +792,7 @@ fn point_header(app: &mut SheetApp, wb: &Workbook, sh: &Sheet, range: RangeRef,
} else {
range
};
let reference = range.a1();
let reference =
if ed.sheet != wb.active_sheet() { format!("{}!{reference}", gridcraft_engine::formula::quote_sheet(&sh.name)) } else { reference };
let reference = header_reference(ed, wb, sh, range);
if add {
ed.append_ref(&reference);
} else {
@@ -787,9 +801,49 @@ fn point_header(app: &mut SheetApp, wb: &Workbook, sh: &Sheet, range: RangeRef,
ed.point_anchor = None;
ed.point_cell = Some(range.start);
ed.request_focus = true;
// The drag keeps the end of the reference the press didn't land on.
app.grid.drag = if range.is_full_cols() {
Drag::PointCols(if range.start.col == pressed.start.col { range.end.col } else { range.start.col })
} else {
Drag::PointRows(if range.start.row == pressed.start.row { range.end.row } else { range.start.row })
};
true
}
fn header_reference(ed: &EditState, wb: &Workbook, sh: &Sheet, range: RangeRef) -> String {
let reference = range.a1();
if ed.sheet != wb.active_sheet() { format!("{}!{reference}", gridcraft_engine::formula::quote_sheet(&sh.name)) } else { reference }
}
/// The frame that starts a drag reports no `dragged()`, so its movement is applied here.
fn point_header_first_move(app: &mut SheetApp, input: &InteractionInput<'_>) {
if input.response.drag_started()
&& let Some(p) = input.position
{
let drag = app.grid.drag.clone();
point_header_to(app, input.geo, input.sheet, input.workbook, &drag, p);
}
}
/// Point mode dragged over the headers: the pointed reference runs from the anchor to the pointer.
fn point_header_to(app: &mut SheetApp, geo: &Geo, sh: &Sheet, wb: &Workbook, drag: &Drag, p: Pos2) {
let range = match *drag {
Drag::PointCols(anchor) => {
let col = geo.col_at(sh, p.x.clamp(geo.cells.left() + 1.0, geo.cells.right() - 1.0));
RangeRef::cols(anchor.min(col), anchor.max(col))
}
Drag::PointRows(anchor) => {
let row = geo.row_at(sh, p.y.clamp(geo.cells.top() + 1.0, geo.cells.bottom() - 1.0));
RangeRef::rows(anchor.min(row), anchor.max(row))
}
_ => return,
};
let Some(ed) = app.editor.as_mut().filter(|ed| ed.point_at_caret()) else { return };
let reference = header_reference(ed, wb, sh, range);
ed.insert_ref(&reference);
ed.point_cell = Some(range.start);
}
fn select_to_pointer(app: &mut SheetApp, geo: &Geo, sh: &Sheet, wb: &Workbook, p: Pos2) {
let cell = CellRef::new(
geo.row_at(sh, p.y.clamp(geo.cells.top() + 1.0, geo.cells.bottom() - 1.0)),
@@ -173,3 +173,92 @@ fn shift_does_not_extend_a_row_reference_across_sheets() {
[("6:6".into(), Some("Other sheet".into())),]
);
}
fn press_and_drag(h: &mut egui_kittest::Harness<'_, SheetApp>, from: Pos2, to: Pos2, modifiers: Modifiers) {
h.input_mut().events.push(Event::ModifiersChanged(modifiers));
h.input_mut()
.events
.extend([Event::PointerMoved(from), Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers }]);
h.step();
drag_to(h, to);
}
fn drag_to(h: &mut egui_kittest::Harness<'_, SheetApp>, to: Pos2) {
h.input_mut().events.push(Event::PointerMoved(to));
h.step();
}
fn release(h: &mut egui_kittest::Harness<'_, SheetApp>, at: Pos2) {
h.input_mut().events.push(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
h.run_steps(2);
h.input_mut().events.push(Event::ModifiersChanged(Modifiers::NONE));
}
fn text<'a>(h: &'a egui_kittest::Harness<'_, SheetApp>) -> &'a str {
h.state().editor.as_ref().map_or("", |ed| ed.text.as_str())
}
#[test]
fn dragging_over_column_headers_extends_the_reference() {
let mut h = harness();
let (c, e, a, d) = (header_center(&h, false, 2), header_center(&h, false, 4), header_center(&h, false, 0), header_center(&h, false, 3));
press_and_drag(&mut h, c, e, Modifiers::NONE);
assert_eq!(text(&h), "=SUM(C:E");
// Back past the press: the reference follows the pointer instead of keeping E.
drag_to(&mut h, a);
assert_eq!(text(&h), "=SUM(A:C");
// The release position counts even without a movement frame before it.
release(&mut h, d);
assert_eq!(text(&h), "=SUM(C:D");
assert_eq!(h.state().session.active().unwrap().selection.current().a1(), "F8");
}
#[test]
fn dragging_over_row_headers_extends_the_reference() {
let mut h = harness();
let (r3, r6, r2) = (header_center(&h, true, 2), header_center(&h, true, 5), header_center(&h, true, 1));
press_and_drag(&mut h, r3, r6, Modifiers::NONE);
assert_eq!(text(&h), "=SUM(3:6");
release(&mut h, r2);
assert_eq!(text(&h), "=SUM(2:3");
assert_eq!(h.state().session.active().unwrap().selection.current().a1(), "F8");
}
#[test]
fn header_drag_on_another_sheet_keeps_the_qualifier() {
let mut h = harness();
h.state_mut().session.execute("home.insertSheet", json!({"name": "Other sheet"})).unwrap();
h.run_steps(2);
let (b, d) = (header_center(&h, false, 1), header_center(&h, false, 3));
press_and_drag(&mut h, b, d, Modifiers::NONE);
assert_eq!(text(&h), "=SUM('Other sheet'!B:D");
}
#[test]
fn header_drag_with_command_keeps_the_earlier_argument() {
let mut h = harness();
let (a, e, f) = (header_center(&h, false, 0), header_center(&h, false, 4), header_center(&h, false, 5));
click(&mut h, a);
press_and_drag(&mut h, e, f, Modifiers { mac_cmd: true, command: true, ..Modifiers::NONE });
assert_eq!(text(&h), "=SUM(A:A,E:F");
}
#[test]
fn header_drag_with_shift_keeps_the_far_end_of_the_reference() {
let mut h = harness();
let (c, e, d) = (header_center(&h, false, 2), header_center(&h, false, 4), header_center(&h, false, 3));
click(&mut h, c);
press_and_drag(&mut h, e, d, Modifiers { shift: true, ..Modifiers::NONE });
assert_eq!(text(&h), "=SUM(C:D");
}
#[test]
fn clicks_on_headers_replace_the_reference_after_a_drag_too() {
let mut h = harness();
let (c, e, g) = (header_center(&h, false, 2), header_center(&h, false, 4), header_center(&h, false, 6));
press_and_drag(&mut h, c, e, Modifiers::NONE);
release(&mut h, e);
assert_eq!(text(&h), "=SUM(C:E");
click(&mut h, g);
assert_eq!(text(&h), "=SUM(G:G");
}
+3 -2
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 (dragging over row or column headers in point mode extends the reference; 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
@@ -57,7 +57,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; 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 |
| 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. |
| Point mode (click/drag/arrow references), reference colouring, F4 | done; dragging over row or column headers points at whole rows or columns (`A:C`, `2:5`); 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. |
| Number-format colors (`[Green]`, `[Color1]`–`[Color56]`) | Rendered in grid and PDF from shared RGB mappings (#1452) |
| Point mode (click/drag/arrow references), reference colouring, F4 | done |
| AutoComplete in columns, function autocomplete and argument tips | done; cell helpers clip to the worksheet and hide when the editor scrolls out (#200) |
@@ -237,6 +237,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
| Date | Change | Summary |
|---|---|---|
| 2026-11-03 | minor | Point mode: dragging over row or column headers extends the whole-row or whole-column reference, as it does over cells |
| 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 |