Window: with Open Documents As Tabs cleared, new documents open in windows

Preferences > User Interface > Open Documents As Tabs was stored but did
nothing. With the box cleared, each document opened or created afterwards
opens in a floating window of its own, at the next cascade place, as
Window > Arrange > Float in Window floats it. Documents already open, and
documents open when the app starts, keep their tabs; checking the box
again leaves floating documents in their windows.

The engine records the preference on each document as it opens
(DocState::opened_as_tab), and the UI finds new documents by uid, so a
command batch that closes and opens documents, or changes the preference
between opening them, places each one as the preference was when it
opened.
This commit is contained in:
dotCooCoo
2026-11-03 16:03:39 +07:00
committed by bflatastic
parent ca8aba6d6a
commit 4b683c3a98
9 changed files with 126 additions and 10 deletions
+4
View File
@@ -52,6 +52,9 @@ pub struct DocState {
pub recalled_selection: RecalledSelection,
/// Process-unique id of this open document (tab indices shift when tabs close).
pub uid: u64,
/// Preferences › User Interface › Open Documents As Tabs when the document opened: cleared,
/// the UI opens it in a floating window of its own.
pub opened_as_tab: bool,
/// View Opacity Mask (Alt-click the mask thumbnail): the masked object whose mask the canvas
/// shows alone, in greyscale, while that mask is edited (see [`DocState::shown_mask`]).
pub mask_view: Option<NodeId>,
@@ -255,6 +258,7 @@ impl DocState {
last_selection_cmd: None,
recalled_selection: RecalledSelection::default(),
uid: NEXT_DOC_UID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
opened_as_tab: true,
mask_view: None,
transparency_grid,
format: "vectorcraft",
+2
View File
@@ -209,6 +209,7 @@ impl Session {
self.reset_tool_for_doc_switch();
let mut st = DocState::new(doc, path);
st.history.limit = self.prefs.history_states as usize;
st.opened_as_tab = self.prefs.open_documents_as_tabs;
self.docs.push(st);
let i = self.docs.len() - 1;
self.active = Some(i);
@@ -239,6 +240,7 @@ impl Session {
st.history.limit = old.history.limit;
// Same open document (caches keyed by uid stay valid); a new revision redraws it.
st.uid = old.uid;
st.opened_as_tab = old.opened_as_tab;
st.revision = old.revision + 1;
st.format = old.format;
st.save_options = old.save_options.clone();
+2 -1
View File
@@ -54,6 +54,7 @@ impl VectorcraftApp {
// ([`place::place_from_cursor`]).
session.place_cursor_requests = true;
let views = session.documents().iter().map(View::of).collect();
let known = session.documents().iter().map(|d| d.uid).collect();
Self {
session,
ui: UiState::default(),
@@ -85,7 +86,7 @@ impl VectorcraftApp {
hover_cursor: None,
presented: None,
panes: Default::default(),
windows: Default::default(),
windows: crate::doc_windows::Windows::new(known),
},
perf: Perf::default(),
dock_rt: Default::default(),
+3 -1
View File
@@ -1,7 +1,8 @@
use super::{Value, VectorcraftApp, View, ViewInfo, panels};
impl VectorcraftApp {
/// Keep `views` aligned with the session's documents (a new one starts at its saved view).
/// Keep `views` aligned with the session's documents (a new one starts at its saved view, and
/// opens in a floating window while Open Documents As Tabs is cleared).
pub fn sync_views(&mut self) {
let docs = self.session.documents();
self.views.truncate(docs.len());
@@ -9,6 +10,7 @@ impl VectorcraftApp {
for (view, doc) in self.views.iter_mut().zip(docs) {
view.artboard = view.artboard.min(doc.doc.artboards.len().saturating_sub(1));
}
crate::doc_windows::opened(self);
// Undo can remove the Artboard tool's active or additionally selected boards. Repair only
// invalid indices: valid multiselection, drag state and tool options must survive history.
if self.session.tool_id() != "artboard" || self.session.tool_busy() {
+35
View File
@@ -9,6 +9,9 @@
//! A document tab dragged off the tab strip floats its document in a window under the pointer,
//! and a document window dragged onto the tab strip by its title bar (an OS window by the tab at
//! its top) goes back under the tabs.
//!
//! With Preferences › User Interface › Open Documents As Tabs cleared, each document opened or
//! created afterwards opens in a floating window ([`opened`]).
use serde_json::{Value, json};
@@ -77,6 +80,16 @@ pub struct Windows {
pub(crate) strip: Option<egui::Rect>,
/// Counts the windows opened ([`DocWindow::number`]).
opened: u32,
/// The open documents (their uids) when [`opened`] last ran, or when the app started.
known: Vec<u64>,
}
impl Windows {
/// No windows yet, with `known` (uids) the documents open when the app starts: they keep their
/// tabs, whatever Open Documents As Tabs says.
pub(crate) fn new(known: Vec<u64>) -> Self {
Self { known, ..Default::default() }
}
}
/// Whether a document tab dragged from `strip` to `p` has left it far enough to float.
@@ -142,6 +155,28 @@ fn area(app: &VectorcraftApp) -> egui::Rect {
/// open. Each fits its window.
fn float(app: &mut VectorcraftApp, docs: &[usize]) {
app.sync_views();
place(app, docs);
}
/// The documents opened or created since the last call: each one opened while Preferences › User
/// Interface › Open Documents As Tabs was cleared ([`vectorcraft_engine::DocState::opened_as_tab`])
/// opens in a floating window of its own, at the next cascade place. Documents already open stay
/// where they are. Called by [`VectorcraftApp::sync_views`].
pub(crate) fn opened(app: &mut VectorcraftApp) {
let docs = app.session.documents();
let known = &app.canvas.windows.known;
if known.len() == docs.len() && known.iter().zip(docs).all(|(u, d)| *u == d.uid) {
return;
}
let new: Vec<usize> = docs.iter().enumerate().filter(|(_, d)| !d.opened_as_tab && !known.contains(&d.uid)).map(|(i, _)| i).collect();
app.canvas.windows.known = docs.iter().map(|d| d.uid).collect();
if !new.is_empty() {
place(app, &new);
}
}
/// [`float`] for documents whose views exist.
fn place(app: &mut VectorcraftApp, docs: &[usize]) {
let first = app.ui.floating.len();
let places = cascade_places(area(app), first + docs.len());
for (k, i) in docs.iter().enumerate() {
+68
View File
@@ -594,6 +594,74 @@ fn float_in_window_and_consolidate_take_a_document() {
assert_eq!(app.run("window.arrange.consolidateAllWindows", json!({})).unwrap(), json!({"tiled": false, "floating": []}));
}
#[test]
fn with_open_documents_as_tabs_cleared_new_documents_open_in_floating_windows() {
let mut app = app_with(1);
let ctx = egui::Context::default();
app.run("prefs.set", json!({"key": "openDocumentsAsTabs", "value": false})).unwrap();
assert_eq!(docked(&app), [0], "a document already open stays under its tab");
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
assert_eq!(docked(&app), [0]);
assert_eq!(floating_indices(&app), [1, 2], "each new document opens in a window of its own");
assert_eq!(app.session.active_index(), Some(2), "the newest document is the current one");
assert_ne!(app.ui.floating[0].place.min, app.ui.floating[1].place.min, "the windows open at cascade places");
frame(&mut app, &ctx, vec![]);
let shapes = frame(&mut app, &ctx, vec![]);
assert!(!tab_drawn(&shapes, &app.session.documents()[2].title()), "a document in a window has no tab");
assert!(tab_drawn(&shapes, &app.session.documents()[0].title()));
assert!(body_of(&app, 2).contains_rect(app.canvas_rect.unwrap()), "the new document's window holds the canvas");
}
#[test]
fn documents_open_when_the_app_starts_keep_their_tabs() {
let mut session = Session::new();
session.prefs.open_documents_as_tabs = false;
session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
let mut app = VectorcraftApp::new(session, Default::default());
app.sync_views();
assert_eq!(docked(&app), [0]);
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
assert_eq!(floating_indices(&app), [1]);
}
#[test]
fn a_document_created_in_a_batch_that_closes_another_still_floats() {
let mut app = app_with(2);
app.run("prefs.set", json!({"key": "openDocumentsAsTabs", "value": false})).unwrap();
let steps = json!([
{"command": "file.close", "params": {}},
{"command": "file.new", "params": {"width": 400, "height": 300}},
]);
app.run("command.batch", json!({"commands": steps})).unwrap();
assert_eq!(app.session.documents().len(), 2, "one document closed and one opened");
assert_eq!(docked(&app), [0]);
assert_eq!(floating_indices(&app), [1], "the new document opens in a window of its own");
}
#[test]
fn each_document_in_a_batch_follows_the_preference_when_it_opened() {
let mut app = app_with(1);
let new = json!({"command": "file.new", "params": {"width": 400, "height": 300}});
let tabs = |on: bool| json!({"command": "prefs.set", "params": {"key": "openDocumentsAsTabs", "value": on}});
let steps = json!([tabs(false), new, tabs(true), new, tabs(false)]);
app.run("command.batch", json!({"commands": steps})).unwrap();
assert_eq!(floating_indices(&app), [1], "opened with the box cleared");
assert_eq!(docked(&app), [0, 2], "opened with the box checked");
}
#[test]
fn checking_open_documents_as_tabs_again_leaves_floating_documents_where_they_are() {
let mut app = app_with(0);
app.run("prefs.set", json!({"key": "openDocumentsAsTabs", "value": false})).unwrap();
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
app.run("prefs.set", json!({"key": "openDocumentsAsTabs", "value": true})).unwrap();
assert_eq!(floating_indices(&app), [0], "the floating document stays in its window");
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
assert_eq!(docked(&app), [1], "a new document opens under a tab");
assert_eq!(floating_indices(&app), [0]);
}
#[test]
fn an_os_window_moves_by_its_tab_and_docks_on_the_strip() {
let host = native_host();
+8 -5
View File
@@ -416,13 +416,16 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
the other Units preferences. The model stays in points; any length field reads `13q` or `13h` (`Ha`, `H`) with or
without a space. Not done: no field shows in it (it waits for a source). Tests `east_asian_*` and `q_and_ha_*` in
`crates/doc` and `crates/engine`. Estimate unchanged.
- **Audited (2026-10-30, #394):** every Preferences dialog option but the four below is read by shipped code,
- **Audited (2026-10-30, #394):** every Preferences dialog option but four is read by shipped code,
by its field or its key; Highlight Alternate Glyphs (the canvas's render options), Real-time Drawing and Editing
(tool previews wait for the commit), Show Tool Tips, Anti-aliased Artwork, Use Precise Cursors and Devices ›
Enable Touch Workspace and Touch Gestures included.
- **Missing:** 4 Preferences options stored but not acted on (#394), each waiting on a subsystem of its own:
User Interface › Open Documents As Tabs (off would open documents in floating windows; one window per
document is G7), Hyphenation › Default Language (hyphenation is English-only, with no per-language
Enable Touch Workspace and Touch Gestures included. One of the four, Open Documents As Tabs, acts now (below);
the other three are listed under Missing.
- **Done:** User Interface › Open Documents As Tabs: with the box cleared, each document opened or created
afterwards opens in a floating window of its own (G7's Float in Window); documents already open stay where they
are. Estimate unchanged.
- **Missing:** 3 Preferences options stored but not acted on (#394), each waiting on a subsystem of its own:
Hyphenation › Default Language (hyphenation is English-only, with no per-language
dictionaries), Plug-ins & Scratch Disks › Primary and Secondary (documents are kept in memory; nothing pages to
disk), and Clipboard Handling › Legacy vector clipboard with its Preserve Paths / Preserve Appearance and
Overprints choice (Illustrator's own clipboard flavour, which has no public specification). Also Properties
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -334,7 +334,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po
| Swatch, graphic style and symbol library panels: each library opens in a panel of its own, several at once, that floats, groups, docks and closes like any panel; opening an open library brings its panel forward; Previous / Next Library change the library in place; the panel menu's Persistent reopens the panel at startup with its view and find field (a library loaded from a file is read again; when its file is missing the panel reopens showing the file's path with Locate Library…, and the status bar names the file) | done | #2603; panel ids `library:<kind>:<id>`, `ui.libraryPanel {panel, library?, view?, showFind?, persistent?}`, `ui.libraryPanel.locate {panel, path?}`; the library panels have no sort option, as before |
| DXF export/import preserves intended curve closure | done | #1299; R13–2018 closed SPLINE bit set/read; an open curve ending at its start stays open; knots and control points unchanged; native CIRCLE/ARC export and CNC preset remain pending |
| macOS save sheet: choose the format from a menu | done | #866; the save panel is VectorCraft's own `NSSavePanel` with its Format menu set before the sheet opens (`apps/vectorcraft/src/mac_save_format.rs`); #1855 had withdrawn the menu that was added after the sheet began, which made Save As, Save a Copy and the first Save show no sheet |
| Preferences: every option acts | partial | 4 stored but unused (#394, audited 2026-10-30): Open Documents As Tabs, Hyphenation › Default Language, Scratch Disks, Legacy vector clipboard; the rest act (Units › East Asian Type since 2026-10-31). General › Corner Radius drives the Rounded Rectangle tool and follows its ↑/↓ (2026-10-24) |
| Preferences: every option acts | partial | 3 stored but unused (#394, audited 2026-10-30): Hyphenation › Default Language, Scratch Disks, Legacy vector clipboard; the rest act (Units › East Asian Type since 2026-10-31; Open Documents As Tabs: cleared, new documents open in floating windows). General › Corner Radius drives the Rounded Rectangle tool and follows its ↑/↓ (2026-10-24) |
| Preferences › General › Constrain Angle | done | arrow keys and Shift drags along the turned axes; the Rectangle, Rounded Rectangle, Ellipse and grid tools, point and area type and the shape size dialogs draw along them with the bounding box turned; Shift-drawn polygons and stars keep the turned base orientation; the Line Segment tool's Shift steps count from it (#394, <https://helpx.adobe.com/illustrator/desktop/manage-objects/arrange-objects/rotate-objects-by-specific-angles.html>); arcs, spirals, flares and Pen paths are not turned |
| Tabs panel: a stop drags from where the press landed, a stop just added included | done | #1458 |
| Outline view keeps template layers in Preview, dimmed | done | #1486; a picture placed to be traced stays visible under the outlines |