mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 09:29:44 -06:00
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:
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user