G4: two windows on one workbook without copying it; Arrange All

- The workbook's active sheet is an atomic set in place (Workbook::active_sheet /
  set_active_sheet), so a window swap or sheet switch never clones the
  Arc<Workbook> or marks it dirty. Selections already live in DocState and the
  window views. Save records the sheet of the most recently active window.
- A dialog taken out of its window while it runs goes back to that window when
  a command run from it switched windows (dialogs::show, ui.dialog.confirm).
- view.arrangeAll {arrange?: tiled|horizontal|vertical|cascade, activeWorkbook?}
  returns the visible windows, active first; the desktop moves and sizes the
  native windows over the main window's monitor. New Window and Arrange All
  buttons on the View ribbon; Arrange Windows dialog.
- Docs: ui-parity, hardware-parity, gaps #20, target-app-parity, ROADMAP.
This commit is contained in:
aiongg
2026-10-22 16:50:10 +00:00
parent d4db58cde4
commit 2ed3df2bd6
56 changed files with 620 additions and 224 deletions
+70 -5
View File
@@ -188,7 +188,7 @@ fn chart_category_axis_from_blank_corner_and_select_data() {
assert_eq!(series(&s), [(Some("Sheet1!$D$2:$D$4".to_string()), "Sheet1!$E$2:$E$4".to_string())]);
let d = s.doc().unwrap();
let sh = d.wb.active().unwrap();
let data = gridcraft_chart::resolve(&d.wb, d.wb.active_sheet, sh.charts.last().unwrap());
let data = gridcraft_chart::resolve(&d.wb, d.wb.active_sheet(), sh.charts.last().unwrap());
assert_eq!(data.categories, ["2020", "2021", "2022"]);
// The last series can't be removed; "" clears the labels.
s.execute("chart.selectData", json!({"categories": "", "removeSeries": [0]})).unwrap();
@@ -1608,7 +1608,7 @@ fn slicer_finds_a_named_table_on_another_sheet() {
s.execute("insert.table", json!({"range": "A1:B3", "name": "Sales"})).unwrap();
s.execute("home.insertSheet", json!({"name": "Report"})).unwrap();
let report = s.doc().unwrap().wb.sheet_index("Report").unwrap();
assert_eq!(s.doc().unwrap().wb.active_sheet, report);
assert_eq!(s.doc().unwrap().wb.active_sheet(), report);
// With another sheet active, `table` still names the table, and the slicer goes on the table's sheet.
let r = s.execute("insert.slicer", json!({"table": "Sales", "column": "Region", "at": "D2"})).unwrap();
@@ -3843,7 +3843,7 @@ fn line_sizes_and_freeze_panes_take_a_sheet() {
assert!(data.cols.get(&4).is_some_and(|c| c.hidden));
assert_eq!(data.default_col_width, 80.0);
assert_eq!(sheet(&s, 0), before, "the active sheet is untouched");
assert_eq!(s.doc().unwrap().wb.active_sheet, 0);
assert_eq!(s.doc().unwrap().wb.active_sheet(), 0);
s.execute("view.freezePanes", json!({"cell": "Data!B3"})).unwrap();
assert_eq!(sheet(&s, 1).freeze, Some((2, 1)));
@@ -4550,11 +4550,11 @@ fn switching_windows_swaps_their_views_without_dirtying_the_workbook() {
s.execute("window.activate", json!({"window": first})).unwrap();
let d = s.doc().unwrap();
assert_eq!((d.wb.active_sheet, d.selection.a1()), (0, "B2:C3".to_string()));
assert_eq!((d.wb.active_sheet(), d.selection.a1()), (0, "B2:C3".to_string()));
assert!(!d.is_dirty(), "a window's sheet and selection are not document changes");
s.execute("view.switchWindows", json!({"window": second})).unwrap();
let d = s.doc().unwrap();
assert_eq!((d.wb.active_sheet, d.selection.a1()), (1, "D4".to_string()));
assert_eq!((d.wb.active_sheet(), d.selection.a1()), (1, "D4".to_string()));
assert!(!d.is_dirty());
// A routed command acts in its window and leaves the active one showing.
@@ -4564,6 +4564,71 @@ fn switching_windows_swaps_their_views_without_dirtying_the_workbook() {
assert_eq!(s.doc().unwrap().selection.a1(), "D4");
}
#[test]
fn window_swaps_share_the_workbook_and_save_records_the_latest_window_sheet() {
let mut s = s();
s.execute("home.insertSheet", json!({})).unwrap();
let path = std::env::temp_dir().join(format!("gridcraft-window-arc-{}.xlsx", std::process::id()));
s.execute("file.save", json!({"path": path})).unwrap();
s.execute("sheet.activate", json!({"sheet": 0})).unwrap();
s.execute("selection.set", json!({"range": "B2"})).unwrap();
let first = s.active_window().unwrap();
let second = s.execute("view.newWindow", json!({})).unwrap()["window"].as_u64().unwrap();
s.execute("sheet.activate", json!({"sheet": 1})).unwrap();
s.execute("selection.set", json!({"range": "C3:D4"})).unwrap();
// Swapping windows, as the interface does before drawing each one, never copies the workbook.
let before = s.doc().unwrap().wb.clone();
for _ in 0..3 {
s.show_window(first);
assert_eq!((s.doc().unwrap().wb.active_sheet(), s.doc().unwrap().selection.a1()), (0, "B2".to_string()));
s.activate_window(second);
assert_eq!((s.doc().unwrap().wb.active_sheet(), s.doc().unwrap().selection.a1()), (1, "C3:D4".to_string()));
s.execute("sheet.activate", json!({"sheet": 0})).unwrap();
s.execute("sheet.activate", json!({"sheet": 1})).unwrap();
}
let d = s.doc().unwrap();
assert!(std::sync::Arc::ptr_eq(&before, &d.wb), "a window swap or sheet switch copied the workbook");
assert!(!d.is_dirty());
// A command run in window 2 edits window 2's sheet at its selection.
s.execute("cell.set", json!({"input": "two"})).unwrap();
let d = s.doc().unwrap();
assert_eq!(d.wb.sheets[1].value(CellRef::parse("C3").unwrap()), Value::text("two"));
assert_eq!(d.wb.sheets[0].value(CellRef::parse("B2").unwrap()), Value::Empty);
s.show_window(first);
assert_eq!(s.doc().unwrap().wb.active_sheet(), 0);
// The most recently active window is window 2: the file records its sheet.
s.execute_in(crate::Target::Window(second), "file.save", json!({"path": path})).unwrap();
let mut t = Session::new();
t.execute("file.open", json!({"path": path})).unwrap();
let _ = std::fs::remove_file(&path);
assert_eq!(t.doc().unwrap().wb.active_sheet(), 1);
s.activate_window(first);
s.execute("file.save", json!({"path": path})).unwrap();
let mut t = Session::new();
t.execute("file.open", json!({"path": path})).unwrap();
let _ = std::fs::remove_file(&path);
assert_eq!(t.doc().unwrap().wb.active_sheet(), 0);
}
#[test]
fn arrange_all_picks_the_visible_windows_active_first() {
let mut s = s();
let book1 = s.active_window().unwrap();
let book1b = s.execute("view.newWindow", json!({})).unwrap()["window"].as_u64().unwrap();
s.execute("file.new", json!({})).unwrap();
let book2 = s.active_window().unwrap();
let r = s.execute("view.arrangeAll", json!({})).unwrap();
assert_eq!(r, json!({"arrange": "tiled", "windows": [book2, book1, book1b]}));
s.execute("window.activate", json!({"window": book1b})).unwrap();
s.execute("view.hideWindow", json!({"window": book2})).unwrap();
let r = s.execute("view.arrangeAll", json!({"arrange": "cascade", "activeWorkbook": true})).unwrap();
assert_eq!(r, json!({"arrange": "cascade", "windows": [book1b, book1]}));
assert!(s.execute("view.arrangeAll", json!({"arrange": "diagonal"})).is_err());
}
#[test]
fn hide_unhide_and_switch_by_window_id() {
let mut s = s();