From 568b754211b1473b039208f7114ce2ea9f22eac4 Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:10:49 +0200 Subject: [PATCH] Ctrl+click on an already selected header or cell deselects it (#359) --- crates/ui-egui/src/grid.rs | 41 ++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/crates/ui-egui/src/grid.rs b/crates/ui-egui/src/grid.rs index 29085bd..2581300 100644 --- a/crates/ui-egui/src/grid.rs +++ b/crates/ui-egui/src/grid.rs @@ -1479,9 +1479,7 @@ fn interact(app: &mut SheetApp, ui: &mut egui::Ui, resp: &egui::Response, geo: & d.selection.anchor = sel.anchor; } } else if mods.command { - let mut ranges: Vec = sel.ranges.iter().map(|r| r.a1()).collect(); - ranges.push(c.a1()); - let _ = app.session.run("selection.set", json!({"range": ranges.join(","), "active": c.a1()})); + select_ranges(app, RangeRef::cell(c), true, c); } else { let _ = app.session.run("selection.set", json!({"cell": c.a1()})); if primary_clicked && wb.styles.get(sh.style_id(c)).num_fmt.as_str() == "checkbox" { @@ -1903,11 +1901,32 @@ pub fn follow_link(app: &mut SheetApp, target: &str) { } } +/// The areas after a click: with `add` (Ctrl) the clicked area is added, or removed when it is already +/// selected (unless it is the only one, so the selection never ends up empty). +fn toggled_ranges(mut ranges: Vec, clicked: String, add: bool) -> Vec { + if !add { + return vec![clicked]; + } + if ranges.len() > 1 && ranges.contains(&clicked) { + ranges.retain(|x| *x != clicked); + } else if !ranges.contains(&clicked) { + ranges.push(clicked); + } + ranges +} + fn select_ranges(app: &mut SheetApp, r: RangeRef, add: bool, active: CellRef) { - let mut ranges: Vec = + let current: Vec = if add { app.session.active().map(|d| d.selection.ranges.iter().map(|x| x.a1()).collect()).unwrap_or_default() } else { vec![] }; - ranges.push(r.a1()); - let _ = app.session.run("selection.set", json!({"range": ranges.join(","), "active": active.a1()})); + let clicked = r.a1(); + let ranges = toggled_ranges(current, clicked.clone(), add); + // A removed area takes the active cell with it; `selection.set` then puts it at the start of the last area. + let params = if ranges.contains(&clicked) { + json!({"range": ranges.join(","), "active": active.a1()}) + } else { + json!({"range": ranges.join(",")}) + }; + let _ = app.session.run("selection.set", params); } /// Grid keyboard handling when no text field has focus. @@ -2590,6 +2609,16 @@ mod pane_tests { assert!(!range_in_pane(sel, corner.0, corner.1)); } + #[test] + fn ctrl_click_on_a_selected_header_deselects_it() { + let s = |v: &[&str]| v.iter().map(|x| x.to_string()).collect::>(); + assert_eq!(toggled_ranges(s(&["A:A", "C:C"]), "C:C".into(), true), s(&["A:A"])); + assert_eq!(toggled_ranges(s(&["A:A"]), "C:C".into(), true), s(&["A:A", "C:C"])); + assert_eq!(toggled_ranges(s(&["A:A"]), "A:A".into(), true), s(&["A:A"])); + assert_eq!(toggled_ranges(s(&["A1", "B2", "3:3"]), "B2".into(), true), s(&["A1", "3:3"])); + assert_eq!(toggled_ranges(s(&["A:A", "C:C"]), "C:C".into(), false), s(&["C:C"])); + } + #[test] fn selection_across_the_freeze_line_is_drawn_in_both_panes() { let sel = rng(5, 0, 7, 6);