Sheet tabs: Ctrl-drag (Cmd or Option on macOS) drops a copy of the sheet (fixes #971)

This commit is contained in:
Marco Arnulfo
2026-10-13 12:21:52 +02:00
parent 84acf6293b
commit 4d5360fd90
2 changed files with 77 additions and 7 deletions
+74 -5
View File
@@ -35,6 +35,26 @@ fn activate_sheet(app: &mut SheetApp, i: usize) {
}
}
/// A tab released with Ctrl held (Cmd on macOS, as for the fill handle) drops a copy of the sheet. Excel for Mac uses
/// Option for this, so Option copies there too.
fn drop_copies_sheet(m: egui::Modifiers, mac: bool) -> bool {
m.command || (mac && m.alt)
}
/// `sheet.move` params for tab `i` dropped `steps` tabs away, or `None` when it lands where it started.
fn tab_drop(i: usize, steps: i64, copy: bool) -> Option<serde_json::Value> {
if steps == 0 {
return None;
}
let to = (i as i64 + steps).max(0) as usize;
if copy {
// A move indexes the list without the sheet, a copy the list with it: to the right, the copy goes one further.
let to = if steps > 0 { to + 1 } else { to };
return Some(json!({"sheet": i, "to": to, "copy": true}));
}
Some(json!({"sheet": i, "to": to}))
}
pub fn sheet_tabs(app: &mut SheetApp, ui: &mut Ui) {
let t = Tokens::get(ui.ctx());
egui::Panel::bottom("sheet_tabs").exact_size(32.0).frame(egui::Frame::NONE.fill(t.tab_bar)).show(ui, |ui| {
@@ -155,15 +175,18 @@ pub fn sheet_tabs(app: &mut SheetApp, ui: &mut Ui) {
app.grid.renaming_tab = Some(i);
app.grid.rename_text = name.clone();
}
// Drag to reorder.
// Drag to reorder; with the copy modifier held, drop a copy instead.
let copy = drop_copies_sheet(ui.input(|inp| inp.modifiers), ui.ctx().os() == egui::os::OperatingSystem::Mac);
if resp.dragged() && copy {
ui.ctx().set_cursor_icon(egui::CursorIcon::Copy);
}
if resp.drag_stopped()
&& let Some(p) = ui.input(|inp| inp.pointer.interact_pos())
{
let dx = p.x - r.center().x;
let steps = (dx / w).round() as i64;
if steps != 0 {
let to = (i as i64 + steps).max(0) as usize;
app.run_or_alert("sheet.move", json!({"sheet": i, "to": to}));
if let Some(params) = tab_drop(i, steps, copy) {
app.run_or_alert("sheet.move", params);
}
}
resp.context_menu(|ui| {
@@ -340,10 +363,56 @@ pub fn status_bar(app: &mut SheetApp, ui: &mut Ui) {
#[cfg(test)]
mod tests {
use super::{navigator_items, rename_needs_focus};
use super::{drop_copies_sheet, navigator_items, rename_needs_focus, tab_drop};
use crate::SheetApp;
use egui::Modifiers;
use gridcraft_engine::model::{Sheet, Visibility};
use serde_json::json;
use std::sync::Arc;
fn workbook_abc() -> SheetApp {
let mut session = gridcraft_engine::Session::default();
session.new_workbook();
let mut app = SheetApp::new(session, Default::default());
app.run("home.insertSheet", json!({})).unwrap();
app.run("home.insertSheet", json!({})).unwrap();
for (i, name) in ["A", "B", "C"].into_iter().enumerate() {
app.run("sheet.rename", json!({"sheet": i, "name": name})).unwrap();
}
app
}
fn names(app: &SheetApp) -> Vec<String> {
app.session.active().unwrap().wb.sheets.iter().map(|s| s.name.clone()).collect()
}
#[test]
fn copy_drop_modifier_is_ctrl_or_cmd_or_option_on_mac() {
// egui reports Ctrl as `command` too off macOS, and Cmd as `mac_cmd` and `command` on macOS.
assert!(drop_copies_sheet(Modifiers::CTRL | Modifiers::COMMAND, false));
assert!(!drop_copies_sheet(Modifiers::ALT, false));
assert!(drop_copies_sheet(Modifiers::MAC_CMD | Modifiers::COMMAND, true));
assert!(drop_copies_sheet(Modifiers::ALT, true));
assert!(!drop_copies_sheet(Modifiers::CTRL, true), "Ctrl+click is the context menu on macOS");
assert!(!drop_copies_sheet(Modifiers::NONE, false));
}
#[test]
fn tab_dropped_with_the_copy_modifier_leaves_a_copy_where_a_move_would_land() {
let mut app = workbook_abc();
app.run("sheet.move", tab_drop(0, 1, true).unwrap()).unwrap();
assert_eq!(names(&app), ["A", "B", "A (2)", "C"]);
let mut app = workbook_abc();
app.run("sheet.move", tab_drop(2, -1, true).unwrap()).unwrap();
assert_eq!(names(&app), ["A", "C (2)", "B", "C"]);
let mut app = workbook_abc();
app.run("sheet.move", tab_drop(0, 1, false).unwrap()).unwrap();
assert_eq!(names(&app), ["B", "A", "C"]);
assert!(tab_drop(1, 0, true).is_none());
}
#[test]
fn rename_field_takes_focus_once_and_lets_go_on_commit() {
assert!(rename_needs_focus(false, false), "first frame: grab focus");
+3 -2
View File
@@ -1,6 +1,6 @@
# UI parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (horizontal scrollbar in the sheet tab bar, #840) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (Ctrl/Option-drag a sheet tab drops a copy, #971) · **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
@@ -14,6 +14,7 @@ Sources: `crates/ui-egui/src` (ribbon, grid, editor, formula bar, dialogs, panes
| Element | Excel | GridCraft | Status |
|---|---|---|---|
| 2026-10-13 | minor | Dragging a sheet tab with Ctrl held (Cmd or Option on macOS) drops a copy of the sheet where the tab is released, with the copy cursor (#971) |
| Title bar, Quick Access Toolbar, AutoSave toggle | yes | yes | done; macOS window controls misaligned (#170), Dock icon size (#169) |
| Ribbon: tabs, groups, split buttons, galleries, contextual tabs | yes | yes (Table Design, Chart Design, Format, PivotTable, Header & Footer tabs) | done; missing contextual tabs for pictures, shapes, sparklines, slicers |
| Ribbon collapse, simplified ribbon, customize ribbon/QAT | yes | partial | customization missing (File ▸ Options is the one missing File command) |
@@ -21,7 +22,7 @@ Sources: `crates/ui-egui/src` (ribbon, grid, editor, formula bar, dialogs, panes
| Ribbon collapse, simplified ribbon, customize ribbon/QAT | yes | partial | customization missing |
| Key tips (Alt / F10 then letters) | yes, every ribbon control | **initial** (#43, 10-10): Alt or F10 shows key tips on Home for alignment, Paste Special and AutoFit plus legacy Alt sequences; other tabs and controls not yet (#33, #49) | 3–5 h |
| Formula bar with Name Box, expand, function hints | yes | yes | done |
| Sheet tabs: rename, reorder, colour, hide, scroll | yes | yes | active tab revealed on navigation and resize (#681); manual wheel/trackpad browsing preserved; rename focus bug on macOS (#145) |
| Sheet tabs: rename, reorder, colour, hide, scroll | yes | yes | Ctrl-drag (Cmd or Option on macOS) drops a copy (#971); active tab revealed on navigation and resize (#681); manual wheel/trackpad browsing preserved; rename focus bug on macOS (#145) |
| Status bar: Sum/Count/Average and more, view buttons, zoom | yes | yes | customizable statistics partial |
| Task panes (Format Chart, Comments, Watch, Selection, PivotTable fields) | yes | yes | element-level Format pane for shapes and pictures missing |
| Backstage (File) view: Info, New with templates, Open recent, Export, Options | yes | partial | Options dialog and template gallery missing |