Fix #3465: Swatch Libraries Other Library lists .ase on Linux

## Summary
Linux file portals grey out files that do not match the open dialog's default
filter. Other Library… and Locate Library… used File › Open filters, whose
first entry is All readable files and omits swatch-library extensions such as
.ase. Add library_open_filters with the library formats first (matching
Illustrator's Swatch Libraries › Other Library…) and use it from swatch and
graphic style library pickers.

## Tests
- engine: library_open_filters_list_library_formats_first
- ui-egui: other_swatch_library_asks_off_the_ui_thread asserts Swatch libraries
  is the default filter and includes ase

Closes storytold/vectorcraft#3465

Co-authored-by: nrq6993-a11y <nrq6993-a11y@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-10-22 07:21:53 +07:00
committed by bflatastic
co-authored by nrq6993-a11y
parent 1388b8ec1a
commit 1fbec667ec
6 changed files with 45 additions and 7 deletions
+11
View File
@@ -793,6 +793,17 @@ pub fn open_filters() -> impl Iterator<Item = (&'static str, &'static [&'static
.chain(std::iter::once(("Plug-ins", super::plugin::EXTS)))
}
/// Open-dialog filters for Swatch Libraries and Graphic Style Libraries Other Library… and Locate
/// Library…. The library formats come first so Linux file portals (which grey out files that don't
/// match the default filter) still list `.ase` and the other library extensions; then the same
/// filters as [`open_filters`] so a document can be picked for its swatches or graphic styles.
pub fn library_open_filters(
library_label: &'static str,
library_exts: &'static [&'static str],
) -> impl Iterator<Item = (&'static str, &'static [&'static str])> {
std::iter::once((library_label, library_exts)).chain(open_filters())
}
/// File → Place dialog filters: "All placeable files", then one per placeable format, then text,
/// RTF and Word.
pub fn place_filters() -> impl Iterator<Item = (&'static str, &'static [&'static str])> {
+8
View File
@@ -384,6 +384,14 @@ fn open_exts_cover_every_readable_format() {
assert_eq!(filters.last(), Some(&("Plug-ins", crate::cmd::plugin::EXTS)), "File › Open installs plug-ins");
}
#[test]
fn library_open_filters_list_library_formats_first() {
let filters: Vec<_> = library_open_filters("Swatch libraries", super::swatchlib::LIBRARY_EXTS).collect();
assert_eq!(filters[0], ("Swatch libraries", super::swatchlib::LIBRARY_EXTS));
assert!(filters[0].1.contains(&"ase"));
assert_eq!(filters[1], ("All readable files", OPEN_EXTS));
}
/// The extensions usage texts list for export: one per writable format, each picking that format
/// (`.png` picks PNG; PNG-8 is asked for by `format`).
#[test]
+1 -1
View File
@@ -767,7 +767,7 @@ pub(crate) fn load_library(app: &mut VectorcraftApp, params: Value) -> Result<Va
/// Other Library…: load the library (or document) at `path`, else one picked in an open dialog.
pub(crate) fn other_library(app: &mut VectorcraftApp, path: Option<String>) -> Result<Value, String> {
match library_panel::pick_library_file(app, path)? {
match library_panel::pick_library_file(app, path, library_panel::library_pick_filters(GraphicStyleLibraries::KIND))? {
Some(path) => load_library(app, json!({ "path": path })),
None => Ok(Value::Null),
}
+20 -4
View File
@@ -17,7 +17,9 @@ use std::sync::{Mutex, PoisonError};
use egui::{Rect, Sense, Ui};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use vectorcraft_engine::cmd::swatchlib::LibraryInfo;
use vectorcraft_doc::style_libs::STYLES_EXT;
use vectorcraft_engine::cmd::fileio;
use vectorcraft_engine::cmd::swatchlib::{self, LibraryInfo};
use super::swatches::{View, click_selection, folder_tile, list_row, list_row_name, tile_grid};
use super::{pstate, set_pstate};
@@ -503,7 +505,8 @@ pub(crate) fn locate(app: &mut VectorcraftApp, p: &Value) -> Result<Value, Strin
return Err(format!("library panel `{given}` isn't missing its library"));
}
let path = p.get("path").and_then(Value::as_str).map(str::to_string);
let Some(path) = pick_library_file(app, path)? else { return Ok(Value::Null) };
let filters = library_pick_filters(k.kind);
let Some(path) = pick_library_file(app, path, filters)? else { return Ok(Value::Null) };
let r = app.run(k.load, json!({ "path": path }))?;
let next = r["library"].as_str().unwrap_or_default().to_string();
let to = panel_id(k.kind, &next);
@@ -529,9 +532,22 @@ fn set(app: &mut VectorcraftApp, panel: &str, mut params: Value) {
}
}
/// Open-dialog filters for Other Library… / Locate Library… of library panels of kind `kind`.
pub(crate) fn library_pick_filters(kind: &str) -> Vec<(&'static str, &'static [&'static str])> {
match kind {
super::swatches::SwatchLibraries::KIND => fileio::library_open_filters("Swatch libraries", swatchlib::LIBRARY_EXTS).collect(),
super::graphic_styles::GraphicStyleLibraries::KIND => fileio::library_open_filters("Graphic Style Libraries", &[STYLES_EXT]).collect(),
_ => fileio::open_filters().collect(),
}
}
/// Other Library…: the library file at `path`, else one picked in an open dialog; `None` when the
/// pick is asynchronous (on the web the picked file arrives later through [`crate::io::open_bytes`]).
pub(crate) fn pick_library_file(app: &mut VectorcraftApp, path: Option<String>) -> Result<Option<String>, String> {
pub(crate) fn pick_library_file(
app: &mut VectorcraftApp,
path: Option<String>,
filters: Vec<(&'static str, &'static [&'static str])>,
) -> Result<Option<String>, String> {
if path.is_none()
&& let Some(f) = app.services.open_async.as_mut()
{
@@ -539,7 +555,7 @@ pub(crate) fn pick_library_file(app: &mut VectorcraftApp, path: Option<String>)
return Ok(None);
}
path.or_else(|| {
let pick = crate::FilePick { filters: crate::FilePick::filters(vectorcraft_engine::cmd::fileio::open_filters()), ..Default::default() };
let pick = crate::FilePick { filters: crate::FilePick::filters(filters), ..Default::default() };
crate::picks::open(app, &pick)
})
.map(Some)
+1 -1
View File
@@ -1214,7 +1214,7 @@ pub(crate) fn load_library(app: &mut VectorcraftApp, params: Value) -> Result<Va
/// Other Library…: load the library (or document) at `path`, else one picked in an open dialog
/// (on the web the picked file arrives later and opens through [`crate::io::open_bytes`]).
pub(crate) fn other_library(app: &mut VectorcraftApp, path: Option<String>) -> Result<Value, String> {
match library_panel::pick_library_file(app, path)? {
match library_panel::pick_library_file(app, path, library_panel::library_pick_filters(SwatchLibraries::KIND))? {
Some(path) => load_library(app, json!({ "path": path })),
None => Ok(Value::Null),
}
+4 -1
View File
@@ -260,7 +260,10 @@ fn other_swatch_library_asks_off_the_ui_thread() {
app.run("swatch.new", json!({"name": "Signal", "color": "#ff3300"})).unwrap();
let (dir, file) = saved_document(&mut app, "swatches");
click(&mut app, "Other Library…", library_menu::<SwatchLibraries>);
assert!(matches!(shown.requests().as_slice(), [PickRequest::Open(_)]));
assert!(matches!(
shown.requests().as_slice(),
[PickRequest::Open(p)] if p.filters.first().is_some_and(|(label, exts)| label == "Swatch libraries" && exts.iter().any(|e| e == "ase"))
));
poll(&mut app);
assert!(library_panel::last_open(&app.ui).is_none() && app.ui.status.is_empty(), "{}", app.ui.status);
shown.answer(&[&file]);