mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 20:08:42 +00:00
Documents: closing a background tab keeps the document in front
file.close {index} on a tab other than the current one made the
document now at the closed index current and reset the tool, so the
document the user was working in went to the back. The current
document now stays current, found by its id as document.move does, and
its tool keeps its state. Closing the current tab still makes its
neighbor current.
Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -404,11 +404,18 @@ impl Session {
|
||||
true
|
||||
}
|
||||
|
||||
/// Close the document in tab `index` (`file.close`). Closing a tab other than the current one
|
||||
/// keeps the current document current, and its tool keeps its state: no document is switched
|
||||
/// to. Closing the current document makes the tab now at `index` current, or the last tab when
|
||||
/// it was the last one. False, changing nothing, when `index` is past the last document.
|
||||
pub fn close_document(&mut self, index: usize) -> bool {
|
||||
if index >= self.docs.len() {
|
||||
return false;
|
||||
}
|
||||
self.reset_tool_for_doc_switch();
|
||||
let front = self.active().map(|d| d.uid).filter(|_| self.active != Some(index));
|
||||
if front.is_none() {
|
||||
self.reset_tool_for_doc_switch();
|
||||
}
|
||||
// Closed (saved or discarded): nothing left to recover.
|
||||
let uid = self.docs[index].uid;
|
||||
cmd::recovery::forget(self, uid);
|
||||
@@ -416,6 +423,10 @@ impl Session {
|
||||
if let Some(stash) = &mut self.batch_stash {
|
||||
stash.push(old);
|
||||
}
|
||||
if let Some(uid) = front {
|
||||
self.active = self.docs.iter().position(|d| d.uid == uid);
|
||||
return true;
|
||||
}
|
||||
self.active = if self.docs.is_empty() { None } else { Some(index.min(self.docs.len() - 1)) };
|
||||
self.sync_front_composite_fonts();
|
||||
self.sync_front_profiles();
|
||||
|
||||
@@ -47,6 +47,7 @@ mod dashalign;
|
||||
mod dcs;
|
||||
mod distort;
|
||||
mod docsetup;
|
||||
mod document_close;
|
||||
mod document_move;
|
||||
mod draw2;
|
||||
mod dynamic_symbols;
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
//! `file.close {index}`: closing a document tab other than the current one keeps the current
|
||||
//! document current, and closing the current one makes its neighbor current.
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::*;
|
||||
|
||||
/// A session with documents A, B, C and D, in that order, D current.
|
||||
fn four() -> Session {
|
||||
let mut s = Session::new();
|
||||
for name in ["A", "B", "C", "D"] {
|
||||
s.execute("file.new", &json!({"name": name, "created": null})).unwrap();
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn order(s: &Session) -> String {
|
||||
s.documents().iter().map(DocState::title).collect::<Vec<_>>().join("")
|
||||
}
|
||||
|
||||
fn current(s: &Session) -> String {
|
||||
s.active().map(DocState::title).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn close(s: &mut Session, index: usize) {
|
||||
s.execute("file.close", &json!({"index": index})).unwrap_or_else(|e| panic!("file.close {index}: {e}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_a_tab_before_the_current_one_keeps_the_current_document() {
|
||||
let mut s = four();
|
||||
close(&mut s, 0);
|
||||
assert_eq!((order(&s), current(&s)), ("BCD".into(), "D".into()));
|
||||
assert_eq!(s.active_index(), Some(2));
|
||||
assert!(s.set_active(1));
|
||||
close(&mut s, 0);
|
||||
assert_eq!((order(&s), current(&s)), ("CD".into(), "C".into()));
|
||||
assert_eq!(s.active_index(), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_a_tab_after_the_current_one_keeps_the_current_document() {
|
||||
let mut s = four();
|
||||
assert!(s.set_active(1));
|
||||
close(&mut s, 3);
|
||||
assert_eq!((order(&s), current(&s)), ("ABC".into(), "B".into()));
|
||||
close(&mut s, 2);
|
||||
assert_eq!((order(&s), current(&s)), ("AB".into(), "B".into()));
|
||||
assert_eq!(s.active_index(), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_the_current_tab_makes_its_neighbor_current() {
|
||||
let mut s = four();
|
||||
assert!(s.set_active(1));
|
||||
close(&mut s, 1);
|
||||
assert_eq!((order(&s), current(&s)), ("ACD".into(), "C".into()), "the tab that took its place");
|
||||
assert!(s.set_active(2));
|
||||
close(&mut s, 2);
|
||||
assert_eq!((order(&s), current(&s)), ("AC".into(), "C".into()), "the last tab: the one before it");
|
||||
s.execute("file.close", &json!({})).unwrap();
|
||||
assert_eq!((order(&s), current(&s)), ("A".into(), "A".into()), "without an index, the current one closes");
|
||||
close(&mut s, 0);
|
||||
assert!(s.active().is_none());
|
||||
assert!(s.execute("file.close", &json!({"index": 0})).is_err(), "no document left");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_another_tab_keeps_the_tool_and_the_documents_edits() {
|
||||
let mut s = four();
|
||||
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap();
|
||||
let resets = s.tool_resets;
|
||||
let nodes = s.doc().unwrap().doc.node_count();
|
||||
let history = super::history(&s);
|
||||
close(&mut s, 1);
|
||||
assert_eq!(s.tool_resets, resets, "no document was switched to, so the tool is not reset");
|
||||
assert_eq!(super::history(&s), history);
|
||||
assert_eq!(s.doc().unwrap().doc.node_count(), nodes);
|
||||
assert!(s.doc().unwrap().is_dirty(), "the document keeps its unsaved edit");
|
||||
close(&mut s, 2);
|
||||
assert_ne!(s.tool_resets, resets, "closing the current document resets the tool");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bad_index_closes_nothing() {
|
||||
let mut s = four();
|
||||
assert!(s.set_active(2));
|
||||
assert!(s.execute("file.close", &json!({"index": 4})).is_err());
|
||||
assert!(!s.close_document(4));
|
||||
assert_eq!((order(&s), current(&s)), ("ABCD".into(), "C".into()));
|
||||
}
|
||||
@@ -160,8 +160,8 @@ fn views_follow_closes_and_moves_made_in_one_batch() {
|
||||
.unwrap();
|
||||
assert_eq!(order(&app), ["Delta", "Charlie"]);
|
||||
assert_eq!(zooms(&app), [4.0, 3.0], "Delta does not take the closed Alpha's view");
|
||||
// Closing by index made the document in the closed one's place current (Charlie); it shows its own view.
|
||||
assert_eq!((current(&app), app.view().map(|v| v.zoom)), ("Charlie".into(), Some(3.0)));
|
||||
// Closing other documents by index leaves the current one (Delta) current; it shows its own view.
|
||||
assert_eq!((current(&app), app.view().map(|v| v.zoom)), ("Delta".into(), Some(4.0)));
|
||||
// A document opened afterwards starts with a view of its own.
|
||||
app.run("file.new", json!({"width": 100, "height": 100, "name": "Echo"})).unwrap();
|
||||
app.sync_views();
|
||||
|
||||
+4
-1
@@ -6064,7 +6064,10 @@ document. With no document open they fail with "no document open". `document.act
|
||||
the current one. `document.move {from?, to}` reorders open documents the way dragging a document tab along the tab strip
|
||||
does: `from` defaults to the current document, `to` is the destination index, documents between them shift by one, the
|
||||
current document stays current, and each document keeps its own view (#5663). It is an engine command, so headless and
|
||||
remote `command_list` both list it.
|
||||
remote `command_list` both list it. `file.close {index}` on a document other than the current one leaves the current
|
||||
document current, and its tool keeps its state; closing the current document makes the document in the next tab
|
||||
current, or the one before it when it was the last tab. In the desktop app a modified document comes to the front first,
|
||||
to ask whether to save it.
|
||||
`window.arrange.tile` (Window › Arrange › Tile) shows every open document in its own pane, edge to edge in the document
|
||||
area, in a grid of as many columns as the square root of the number of documents, rounded up, and fits each document to
|
||||
its pane; with fewer than two documents open it leaves the view and `tiled` as they are. It returns `{tiled, panes}`.
|
||||
|
||||
@@ -702,6 +702,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Window › Arrange › Float in Window, Float All in Windows and Cascade (G7):** documents float in windows of their own, out of the tab strip ([public source](https://helpx.adobe.com/illustrator/using/customizing-workspace.html)). Float in Window (`window.arrange.floatInWindow`) floats the current document, Float All in Windows (`window.arrange.floatAllInWindows`) every tabbed one, and Cascade (`window.arrange.cascade`) floats every document and stacks the windows from the upper left to the lower right ([public source](https://helpx.adobe.com/illustrator/using/viewing-artwork.html)). On the desktop each window is an OS window; on the web and on Wayland it is a window inside the app's window. The window of the current document holds the canvas; the others draw their documents as Tile's panes do, and a press in one makes its document the current one. Closing a window closes its document, asking to save unsaved changes. Consolidate All Windows brings every document back under the tabs, and Tile brings them back into its grid. The cascade's 30 pt step, the windows' size and when the items are enabled (Cascade with two documents or more, Float in Window for a tabbed current document, Float All in Windows while a document is tabbed, Consolidate All Windows while documents are tiled or floating) are VectorCraft's own. Tests: `crates/ui-egui/src/doc_windows/tests.rs`.
|
||||
- **Dragging a document tab out and a document window back (G7):** a document tab dragged more than 24 pt above or below the tab strip floats its document in a window under the pointer, and a document window dragged onto the tab strip goes back under the tabs ([public source](https://helpx.adobe.com/illustrator/using/customizing-workspace.html)). A window inside the app's window is dragged by its title bar; an OS window by the document's tab at its top, and the window follows the pointer. The 24 pt distance and the tab at the top of an OS window are VectorCraft's own. Tests: `crates/ui-egui/src/doc_windows/tests.rs`.
|
||||
- **Reordering document tabs (G7, #5663):** a document tab dragged along the tab strip moves its document among the others ([public source](https://helpx.adobe.com/illustrator/using/customizing-workspace.html)); a line marks the gap it goes in. The current document stays current, each document keeps its own zoom, centre and artboard, the views Window › New Window opened move with their document, and Window › Next/Previous Document follow the new order. `document.move` (`{from?, to}`) does the same from MCP, the control channel and recordings. `crates/ui-egui/src/tests/tab_reorder.rs`, `crates/engine/src/tests/document_move.rs`.
|
||||
- **Closing a background document tab:** closing a tab other than the current one (`file.close {index}`) keeps the current document current, and its tool keeps its state; closing the current tab still makes its neighbor current. `crates/engine/src/tests/document_close.rs`.
|
||||
- **Export and Import Preferences (#6435):** an optional VectorCraft feature. The Preferences dialog's Export Preferences… saves the applied preferences to a JSON file (`prefs.export`; the web build downloads it), and Import Preferences… applies such a file (`prefs.import`). The file leaves out the folder preferences. The import checks every value first and changes nothing on an error; it keeps the open document's units and records no undo step. The file format is VectorCraft's own. Tests: `crates/engine/src/tests/prefs_file.rs`, `crates/ui-egui/src/prefs_dialog/tests.rs`.
|
||||
- **Preferences › User Interface › Open Documents As Tabs (G7, #394):** checked by default, documents open under tabs in one document window ([public source](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/workspace-overview.html)). With the box cleared, each document opened or created afterwards opens in a floating window of its own, at the next cascade place, as Float in Window floats it; documents already open stay where they are, and checking the box again leaves floating documents in their windows. Tests: `crates/ui-egui/src/doc_windows/tests.rs`.
|
||||
- **Preferences search:** a Search Preferences field above the category list has the keyboard focus when the dialog opens, and Cmd/Ctrl+F gives it back; typing lists the settings it finds, and choosing one shows its category, as the [help page](https://helpx.adobe.com/illustrator/desktop/get-started/preferences-and-settings/set-app-preferences.html) describes. VectorCraft's own rules: the field matches a setting's label, section or category, ignoring case, in the interface language or in English; the list replaces the categories while the field has text and shows every match, or "No settings found"; the chosen setting's row is highlighted and scrolled into view; Escape in the field with text clears it, and Escape in the empty field closes the dialog. The dialog's `__search` field (`ui.dialog.set`) holds the text. Tests: `crates/ui-egui/src/prefs_dialog/tests.rs`.
|
||||
|
||||
Reference in New Issue
Block a user