mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-24 10:46:50 -06:00
Sheet tabs: Ctrl-drag (Cmd or Option on macOS) drops a copy of the sheet (fixes #971)
This commit is contained in:
@@ -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
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user