mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 05:24:44 +00:00
Merge pull request #7313 from dotCooCoo/fix/background-document-bounds
Type: a document replaced in the back measures its type when it is replaced
This commit is contained in:
@@ -536,6 +536,80 @@ fn a_document_replaced_in_the_back_is_measured_when_the_front_one_closes() {
|
||||
assert_eq!(actual.cached_bounds, Some(expected));
|
||||
}
|
||||
|
||||
/// The type of document `index` with id `id`, wherever its tab is.
|
||||
fn text_in(s: &Session, index: usize, id: u64) -> vectorcraft_doc::TextObject {
|
||||
match &s.documents()[index].doc.node(NodeId(id)).unwrap().kind {
|
||||
NodeKind::Text(t) => (**t).clone(),
|
||||
_ => panic!("not text"),
|
||||
}
|
||||
}
|
||||
|
||||
/// A document replaced while in the back has its type measured in its own composite fonts at once,
|
||||
/// while the front document stays in front with its own definition registered; coming to the front
|
||||
/// does not measure that type again.
|
||||
#[test]
|
||||
fn a_document_replaced_in_the_back_is_measured_before_it_comes_to_the_front() {
|
||||
let (bytes, id, width) = full_roman_file("CF Pane");
|
||||
let mut s = session();
|
||||
s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
|
||||
s.execute("type.compositeFonts.set", &json!({"fonts": [font("CF Pane")]})).unwrap();
|
||||
assert!(s.replace_document(0, vectorcraft_format::load(&bytes).unwrap()));
|
||||
assert_eq!(s.active_index(), Some(1));
|
||||
let front = vectorcraft_text::FontDb::global().composite_font("CF Pane").unwrap();
|
||||
assert_eq!(front.parts.get(vectorcraft_doc::CharClass::Roman).size, 50.0);
|
||||
let measured = text_in(&s, 0, id);
|
||||
assert!((measured.cached_bounds.unwrap().width() - width).abs() < 1e-6);
|
||||
let node = s.documents()[0].doc.node(NodeId(id)).unwrap().geometric_bounds().unwrap();
|
||||
assert!((node.width() - width).abs() < 1e-6);
|
||||
let doc = std::sync::Arc::as_ptr(&s.documents()[0].doc);
|
||||
assert!(s.set_active(0));
|
||||
assert!(std::ptr::eq(doc, std::sync::Arc::as_ptr(&s.documents()[0].doc)));
|
||||
assert_eq!(text(&s, id).cached_bounds, measured.cached_bounds);
|
||||
}
|
||||
|
||||
/// Type a document replaced in the back sets in a composite font that only the front document
|
||||
/// defines is measured in that font while the front one is registered, and again, as missing, when
|
||||
/// the replaced document comes to the front.
|
||||
#[test]
|
||||
fn a_document_replaced_in_the_back_measures_a_font_it_lacks_again_in_front() {
|
||||
let mut s = session();
|
||||
let id = s.execute("text.create", &json!({"x": 10, "y": 50, "text": "Ab12", "size": 20, "font": "CF Lacked"})).unwrap()["id"].as_u64().unwrap();
|
||||
let bytes = vectorcraft_format::save_file(&s.doc().unwrap().doc);
|
||||
s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
|
||||
s.execute("type.compositeFonts.set", &json!({"fonts": [font("CF Lacked")]})).unwrap();
|
||||
assert!(s.replace_document(0, vectorcraft_format::load(&bytes).unwrap()));
|
||||
let back = text_in(&s, 0, id);
|
||||
let expected = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &back).bounds;
|
||||
assert_eq!(back.cached_bounds, Some(expected));
|
||||
assert!(s.set_active(0));
|
||||
assert!(vectorcraft_text::FontDb::global().composite_font("CF Lacked").is_none());
|
||||
let actual = text(&s, id);
|
||||
let expected = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &actual).bounds;
|
||||
assert_eq!(actual.cached_bounds, Some(expected));
|
||||
}
|
||||
|
||||
/// Text holding an inline graphic whose symbol's art is type in a composite font, in a document
|
||||
/// replaced in the back, is measured with the graphic at the size the document's own definition
|
||||
/// gives it before it comes to the front.
|
||||
#[test]
|
||||
fn a_document_replaced_in_the_back_measures_inline_graphics_in_its_own_definition() {
|
||||
let mut s = session();
|
||||
let mut full = font("CF Back Inline");
|
||||
full["parts"]["roman"]["size"] = json!(100);
|
||||
s.execute("type.compositeFonts.set", &json!({"fonts": [full]})).unwrap();
|
||||
let art =
|
||||
s.execute("text.create", &json!({"x": 10, "y": 50, "text": "Ab12", "size": 20, "font": "CF Back Inline"})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("symbol.new", &json!({"name": "CF Back Inline Mark", "ids": [art]})).unwrap();
|
||||
let id = s.execute("text.create", &json!({"x": 10, "y": 150, "text": "x", "size": 20})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("text.insertInline", &json!({"id": id, "at": 0, "symbol": "CF Back Inline Mark"})).unwrap();
|
||||
let width = text(&s, id).cached_bounds.unwrap().width();
|
||||
let bytes = vectorcraft_format::save_file(&s.doc().unwrap().doc);
|
||||
s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
|
||||
s.execute("type.compositeFonts.set", &json!({"fonts": [font("CF Back Inline")]})).unwrap();
|
||||
assert!(s.replace_document(0, vectorcraft_format::load(&bytes).unwrap()));
|
||||
assert!((text_in(&s, 0, id).cached_bounds.unwrap().width() - width).abs() < 1e-6);
|
||||
}
|
||||
|
||||
/// A failed batch takes back the composite fonts it set, in the font database too.
|
||||
#[test]
|
||||
fn a_failed_batch_takes_back_its_composite_fonts() {
|
||||
|
||||
@@ -128,6 +128,11 @@ pub struct DocState {
|
||||
empty_artboards: std::cell::RefCell<EmptyArtboards>,
|
||||
/// What [`DocState::any_art_to_fit`] last measured. Not saved.
|
||||
art_to_fit: std::cell::RefCell<Option<ArtToFit>>,
|
||||
/// The document as [`crate::Session::replace_document`] left it in a tab in the back, with its
|
||||
/// type measured in its own composite fonts, and the lowercased names of those fonts. When the
|
||||
/// same document comes to the front, its type set in them is not measured again. While this
|
||||
/// reference lasts, its allocation holds no other document. Not saved.
|
||||
pub(crate) measured_in_own: Option<(std::sync::Weak<Document>, std::collections::HashSet<String>)>,
|
||||
}
|
||||
|
||||
/// Whether a document has an artboard for Delete Empty Artboards to delete, with the document that
|
||||
@@ -345,6 +350,7 @@ impl DocState {
|
||||
empty_artboards: Default::default(),
|
||||
plugin_effects: Default::default(),
|
||||
art_to_fit: Default::default(),
|
||||
measured_in_own: None,
|
||||
}
|
||||
}
|
||||
/// `measure`'s answer for the document now. `measure` runs once for each version of the document,
|
||||
|
||||
@@ -241,10 +241,18 @@ impl Session {
|
||||
/// Register the composite fonts of the document now in front ([`cmd::compositefonts::sync`]),
|
||||
/// after a switch or a close brought it there. Its type set in a composite font that differs
|
||||
/// from the one of that name before (added, changed or removed) is measured again, and type not
|
||||
/// measured yet (in a document replaced while in the back) is measured now.
|
||||
/// measured yet is measured now. Type that [`Session::replace_document`] measured in the
|
||||
/// document's own composite fonts while it was in the back is not measured again when the
|
||||
/// document is unchanged since.
|
||||
fn sync_front_composite_fonts(&mut self) {
|
||||
let changed: HashSet<String> = cmd::compositefonts::sync(self).into_iter().collect();
|
||||
let mut changed: HashSet<String> = cmd::compositefonts::sync(self).into_iter().collect();
|
||||
let Some(index) = self.active else { return };
|
||||
if let Some(st) = self.docs.get_mut(index)
|
||||
&& let Some((doc, own)) = st.measured_in_own.take()
|
||||
&& std::ptr::eq(doc.as_ptr(), Arc::as_ptr(&st.doc))
|
||||
{
|
||||
changed.retain(|n| !own.contains(n));
|
||||
}
|
||||
self.refresh_document_text_bounds(index, |t| t.cached_bounds.is_none() || sets_in_any(t, &changed));
|
||||
}
|
||||
|
||||
@@ -347,14 +355,10 @@ impl Session {
|
||||
doc.resolve_inline_art();
|
||||
doc.guides_into_nodes();
|
||||
cmd::dimension::conform(&mut doc);
|
||||
// In the back, type set in a font named like a composite font is measured when the document
|
||||
// comes to the front and its own composite fonts are registered, not in the front
|
||||
// document's.
|
||||
let mut back_names = None;
|
||||
if self.active == Some(index) {
|
||||
// Pending tool work (typing, a drag) belongs to the content being thrown away.
|
||||
self.reset_tool_for_doc_switch();
|
||||
} else {
|
||||
// In the back, type set in a font named like a composite font is measured again below, in
|
||||
// the document's own composite fonts, not in the front document's.
|
||||
let back = self.active != Some(index);
|
||||
if back {
|
||||
let mut names = cmd::compositefonts::names_in_use(self);
|
||||
names.extend(doc.composite_fonts.iter().map(|f| f.name.to_lowercase()));
|
||||
for tree in text_trees_mut(&mut doc) {
|
||||
@@ -363,7 +367,9 @@ impl Session {
|
||||
t.cached_baselines.clear();
|
||||
});
|
||||
}
|
||||
back_names = Some(names);
|
||||
} else {
|
||||
// Pending tool work (typing, a drag) belongs to the content being thrown away.
|
||||
self.reset_tool_for_doc_switch();
|
||||
}
|
||||
let old = &self.docs[index];
|
||||
let mut st = DocState::new(doc, old.path.clone());
|
||||
@@ -396,16 +402,36 @@ impl Session {
|
||||
if let Some(stash) = &mut self.batch_stash {
|
||||
stash.push(old);
|
||||
}
|
||||
match back_names {
|
||||
Some(names) => self.refresh_document_text_bounds(index, |t| t.cached_bounds.is_none() && !sets_in_any(t, &names)),
|
||||
None => {
|
||||
self.sync_opened_composite_fonts(index);
|
||||
self.sync_front_profiles();
|
||||
}
|
||||
if back {
|
||||
self.measure_back_document(index);
|
||||
} else {
|
||||
self.sync_opened_composite_fonts(index);
|
||||
self.sync_front_profiles();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Measure the type not measured yet in open document `index`, which is in the back, in its own
|
||||
/// composite fonts, lent for the measuring, so its type has the bounds it will have when the
|
||||
/// document comes to the front. The front document stays the one whose composite fonts are
|
||||
/// registered. The names of the document's composite fonts its type was measured in are kept,
|
||||
/// so its coming to the front does not measure that type again.
|
||||
fn measure_back_document(&mut self, index: usize) {
|
||||
let Some(st) = self.docs.get(index) else { return };
|
||||
let fonts = st.doc.composite_fonts.clone();
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
let lent = db.lend_composite_fonts(&fonts);
|
||||
// A name another loan holds with a different definition is not lent: type set in it is
|
||||
// measured again when the document comes to the front.
|
||||
let own: HashSet<String> =
|
||||
fonts.iter().filter(|f| db.composite_font(&f.name).is_some_and(|have| *have == **f)).map(|f| f.name.to_lowercase()).collect();
|
||||
self.refresh_document_text_bounds(index, |t| t.cached_bounds.is_none());
|
||||
drop(lent);
|
||||
if let Some(st) = self.docs.get_mut(index) {
|
||||
st.measured_in_own = Some((Arc::downgrade(&st.doc), own));
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -711,6 +711,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **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`.
|
||||
- **Replacing a background document's content:** `Session::replace_document` on a tab other than the current one (the replacement File › Revert makes for the current tab) measures the new content's type in that document's own composite fonts at once, so that type has its measured bounds while the document is in the back. When that document becomes the current one unchanged, its type in those fonts is not measured again. `crates/engine/src/cmd/compositefonts/tests.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