From 3d4d34fc15e1c030f8805a29073dd0fcb546a038 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 22 Oct 2026 03:30:07 +0000 Subject: [PATCH] Fix #3395: color book library tabs use file name ## Summary Swatch library panels title imported and User Defined .acb color books from the file name (without the extension), matching Illustrator. Host temporary import paths still fall back to the book title when copying into the user library folder (#2996). ## Tests - `color_book_tab_titles_use_the_file_name` covers user-folder scan, load by path, and copyToUser keeping the source file name. - Existing `copying_a_color_book_from_a_temporary_path_uses_its_title` unchanged. Closes storytold/vectorcraft#3395 Co-authored-by: nrq6993-a11y --- crates/engine/src/cmd/swatchlib.rs | 18 ++++++++--- crates/engine/src/tests_swatchlib.rs | 46 ++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/crates/engine/src/cmd/swatchlib.rs b/crates/engine/src/cmd/swatchlib.rs index 98e3272d0..36c67f696 100644 --- a/crates/engine/src/cmd/swatchlib.rs +++ b/crates/engine/src/cmd/swatchlib.rs @@ -162,7 +162,7 @@ impl Libraries { let file = file_name(&path); let read = || crate::file_access::unconfined(|| read_file(&path)).ok().and_then(|b| L::read(&b, stem(&file)).ok()); let Some(lib) = crate::guard::catch_panic(read).ok().flatten() else { continue }; - let info = LibraryInfo { id: format!("user/{file}"), name: lib.name().to_string(), category: "user" }; + let info = LibraryInfo { id: format!("user/{file}"), name: library_tab_name(&file, lib.name()), category: "user" }; self.extra.push(Extra { info, path: Some(path), lib: Arc::new(lib) }); } } @@ -245,7 +245,7 @@ impl Libraries { if let Some(e) = path.and_then(user) { return Ok((e.info.clone(), e.lib.clone())); } - let info = LibraryInfo { id: format!("loaded/{}", path.unwrap_or(&file)), name: lib.name().to_string(), category: "loaded" }; + let info = LibraryInfo { id: format!("loaded/{}", path.unwrap_or(&file)), name: library_tab_name(&file, lib.name()), category: "loaded" }; let lib = Arc::new(lib); self.extra.retain(|x| x.info.id != info.id); self.extra.push(Extra { info: info.clone(), path: path.map(str::to_string), lib: lib.clone() }); @@ -278,9 +278,10 @@ impl Libraries { Some(p) => { let ext = extension(&p); let base = file_name(&p); - // Color books (.acb) name the tab from the book title inside the file; a sandbox - // copy of an import often lands as tmp….tmp.acb (#2996). - let file = if ext == "acb" || temporary_library_file_name(&base) { library_file_name(lib.name(), &ext) } else { base }; + // A sandbox copy of an import often lands as tmp….tmp.acb (#2996): store it under the + // book title. A normal .acb path keeps its file name so the tab matches Illustrator + // (#3395). + let file = if temporary_library_file_name(&base) { library_file_name(lib.name(), &ext) } else { base }; (file, read_file(&p)?) } None => (library_file_name(lib.name(), ext), encode(&lib)?), @@ -318,6 +319,13 @@ fn temporary_library_file_name(name: &str) -> bool { lower.contains(".tmp.") } +/// The name library panels and menus show: color books (.acb) use the file name without its +/// extension (Illustrator), except when the path is a host temporary (#2996), then the book title. +fn library_tab_name(file: &str, lib_name: &str) -> String { + let base = file_name(file); + if extension(&base) == "acb" && !temporary_library_file_name(&base) { stem(&base).to_string() } else { lib_name.to_string() } +} + /// The file name of library `name` with extension `ext`: characters file systems reject become /// `-`, and dots and spaces at either end are dropped. A name with nothing left is `Library`. pub fn library_file_name(name: &str, ext: &str) -> String { diff --git a/crates/engine/src/tests_swatchlib.rs b/crates/engine/src/tests_swatchlib.rs index 6be4e4f5a..7b17fa586 100644 --- a/crates/engine/src/tests_swatchlib.rs +++ b/crates/engine/src/tests_swatchlib.rs @@ -468,6 +468,52 @@ fn copying_a_color_book_from_a_temporary_path_uses_its_title() { let _ = std::fs::remove_dir_all(dir); } +/// Color books in the user library folder and opened by path show the file name as the tab title +/// (#3395), not only the title stored inside the book. +#[test] +fn color_book_tab_titles_use_the_file_name() { + fn acb_bytes(title: &str) -> Vec { + fn acb_str(out: &mut Vec, s: &str) { + let units: Vec = s.encode_utf16().collect(); + out.extend((units.len() as u32).to_be_bytes()); + units.iter().for_each(|u| out.extend(u.to_be_bytes())); + } + let mut bytes = b"8BCB".to_vec(); + bytes.extend(1u16.to_be_bytes()); + bytes.extend(3000u16.to_be_bytes()); + for s in [title, "PANTONE ", " C", ""] { + acb_str(&mut bytes, s); + } + bytes.extend(1u16.to_be_bytes()); + bytes.extend(7u16.to_be_bytes()); + bytes.extend(0u16.to_be_bytes()); + bytes.extend(0u16.to_be_bytes()); + acb_str(&mut bytes, "185"); + bytes.extend(b"TST001"); + bytes.extend([228, 0, 43]); + bytes.extend(b"spflspot"); + bytes + } + let dir = temp_dir("acb-name"); + let user = dir.join("user"); + let elsewhere = dir.join("elsewhere"); + std::fs::create_dir_all(&user).unwrap(); + std::fs::create_dir_all(&elsewhere).unwrap(); + let on_disk = user.join("PANTONE Solid Coated.acb"); + std::fs::write(&on_disk, acb_bytes("Internal Book Title")).unwrap(); + let named = elsewhere.join("Brand Colors.acb"); + std::fs::write(&named, acb_bytes("Another Internal Title")).unwrap(); + + let mut s = session(); + s.swatch_libraries.set_user_dir(Some(user.to_string_lossy().to_string())); + assert_eq!(run(&mut s, "swatch.library.get", json!({ "library": "user/PANTONE Solid Coated.acb" }))["name"], "PANTONE Solid Coated"); + let loaded = run(&mut s, "swatch.library.load", json!({ "path": named.to_string_lossy() }))["library"].clone(); + assert_eq!(run(&mut s, "swatch.library.get", json!({ "library": loaded }))["name"], "Brand Colors"); + let r = run(&mut s, "swatch.library.copyToUser", json!({ "library": loaded })); + assert_eq!((r["library"].as_str(), r["name"].as_str(), r["copied"].as_bool()), (Some("user/Brand Colors.acb"), Some("Brand Colors"), Some(true))); + let _ = std::fs::remove_dir_all(dir); +} + #[test] fn a_library_group_stays_one_group_when_a_swatch_has_its_name() { use vectorcraft_testkit::ase::{Block, ase};