Browse / github.com/storytold/photocraft / patch
feat(ui-egui): File Handling preference Save As to Original Folder (#9656)
[Feature Request] Save As should default to the open document's folder ### Summary When I open a project file from one folder and then use **File → Save As…**, PhotoCraft opens the Save As dialog in the folder where I last saved a file, not in the folder containing the currently open document. I would like PhotoCraft to offer an option (or make this the default behavior) so that **Save As…** starts in the same directory as the currently open project file. ### Current behavior 1. Open a `.pcraft`, PSD, or other supported project file from, for example, `D:\Projects\ClientA`. 2. Use **File → Save As…**. 3. The dialog opens in the last-used save location, for ex
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- SofiaAFigueredo (imported from pull request #9662, open)
- Base
7ac7b8b106acccf6826d45ed5c9e481dfe341035- Size
- +141 −6 in 27 files
- Review
- pending
- Shared
- 2026-10-28
- Id
29b324127812f8f4ebe0cc72
How it went
No other operator has reported using it yet.
Diff
raw · crates/engine/src/prefs.rs crates/ui-egui/src/app/files.rs crates/ui-egui/src/file_dialog.rs crates/ui-egui/src/file_dialog/tests.rs crates/ui-egui/src/i18n/cs.tsv crates/ui-egui/src/i18n/de.tsv crates/ui-egui/src/i18n/el.tsv crates/ui-egui/src/i18n/es.tsv crates/ui-egui/src/i18n/fr.tsv crates/ui-egui/src/i18n/hu.tsv crates/ui-egui/src/i18n/id.tsv crates/ui-egui/src/i18n/it.tsv crates/ui-egui/src/i18n/ja.tsv crates/ui-egui/src/i18n/ko.tsv crates/ui-egui/src/i18n/nl.tsv crates/ui-egui/src/i18n/pl.tsv crates/ui-egui/src/i18n/pt-br.tsv crates/ui-egui/src/i18n/ru.tsv crates/ui-egui/src/i18n/sr.tsv crates/ui-egui/src/i18n/tr.tsv crates/ui-egui/src/i18n/uk.tsv crates/ui-egui/src/i18n/vi.tsv crates/ui-egui/src/i18n/zh-hans.tsv crates/ui-egui/src/i18n/zh-hant.tsv crates/ui-egui/src/prefs/prefs_ui/fields.rs crates/ui-egui/src/prefs/prefs_ui/tests.rs docs/scorecard.md
@@ -473,6 +473,9 @@ impl Default for HistoryLog { pub struct FileHandling { pub image_previews: Ask, pub lowercase_extension: bool,+ /// Save As and the export dialogs start in the open document's folder (on, the default), or+ /// in the folder last saved or exported to across documents when off (#9656, #8493).+ pub save_as_to_original_folder: bool, /// File › Save a Copy suggests the document's own name rather than "<name> copy" (#8080). pub do_not_append_copy: bool, /// Saves run in the background while you keep working (on), or in the foreground under a@@ -496,6 +499,10 @@ pub struct FileHandling { pub recent_file_count: u32, /// Most recently opened files, newest first (File › Open Recent). pub recent_files: Vec<String>,+ /// The folder the last save or export dialog was answered with, for any document: where+ /// Save As and the export dialogs start with Save As to Original Folder off (#9656). Kept+ /// here so it survives a restart; the dialog does not show it.+ pub last_save_folder: String, /// A folder whose `.cube`, `.3dl` and `.look` files Color Lookup lists under its built-in /// looks (native only, #6558). Empty lists none. pub lut_folder: String,@@ -514,6 +521,7 @@ impl Default for FileHandling { Self { image_previews: Ask::Always, lowercase_extension: true,+ save_as_to_original_folder: true, do_not_append_copy: false, save_in_background: true, autosave: true,@@ -526,6 +534,7 @@ impl Default for FileHandling { rasterize_svg_groups_deeper_than: photocraft_doc::MAX_GROUP_DEPTH as u32, recent_file_count: 20, recent_files: Vec::new(),+ last_save_folder: String::new(), lut_folder: String::new(), } }@@ -199,6 +199,13 @@ impl PhotocraftApp { } }; let doc = st.doc.id;+ // Save As to Original Folder off: the name stays, the folder is the one last saved to.+ // File › Save of a flat file asking for a name keeps writing the document's own file.+ let own_file = plain_save && st.path.as_deref() == Some(suggested.as_str());+ let suggested = match self.last_save_dir().filter(|_| !own_file && !self.session.prefs().file_handling.save_as_to_original_folder) {+ Some(dir) => std::path::Path::new(&suggested).file_name().map_or(suggested.clone(), |name| dir.join(name).to_string_lossy().into_owned()),+ None => suggested,+ }; self.pick_save(&suggested, move |app, path| { app.with_document(doc, |app| { let own = app.session.active().and_then(|st| st.path.clone());@@ -146,17 +146,23 @@ impl PhotocraftApp { /// A bare file name (export dialogs, an untitled document) starts in the folder the active /// document was last saved or exported to through a dialog, else beside the document /// (#1826, #1866). A caller's explicit directory is kept. The chosen folder is remembered for- /// the document the dialog was asked for.+ /// the document the dialog was asked for. With Preferences ▸ File Handling › Save As to+ /// Original Folder off, a bare name starts in the folder last saved or exported to for any+ /// document instead, else beside the document (#9656). pub(crate) fn pick_save(&mut self, suggested: &str, then: impl FnOnce(&mut Self, String) -> Result<Value, String> + 'static) -> Result<Value, String> { let mut suggested = std::path::PathBuf::from(suggested); let doc = self.session.active().map(|d| d.doc.id); // Closed documents' folders are no longer needed. let open: Vec<_> = self.session.documents().iter().map(|d| d.doc.id).collect(); self.save_dirs.retain(|id, _| open.contains(id));+ let beside_document = || self.session.active().and_then(|d| d.path.as_deref()).and_then(|p| std::path::Path::new(p).parent()).map(Path::to_path_buf);+ let remembered = if self.session.prefs().file_handling.save_as_to_original_folder {+ doc.and_then(|d| self.save_dirs.get(&d).cloned())+ } else {+ self.last_save_dir()+ }; if suggested.parent().is_some_and(|p| p.as_os_str().is_empty())- && let Some(dir) = doc- .and_then(|d| self.save_dirs.get(&d).cloned())- .or_else(|| self.session.active().and_then(|d| d.path.as_deref()).and_then(|p| std::path::Path::new(p).parent()).map(Path::to_path_buf))+ && let Some(dir) = remembered.or_else(beside_document) { suggested = dir.join(suggested); }@@ -174,6 +180,10 @@ impl PhotocraftApp { { app.save_dirs.insert(doc, dir.to_path_buf()); }+ if let Some(dir) = Path::new(&path).parent().filter(|p| !p.as_os_str().is_empty()) {+ let dir = dir.to_string_lossy().into_owned();+ app.session.prefs.edit(|p| p.file_handling.last_save_folder = dir);+ } then(app, path) } _ => Err(UNEXPECTED.into()),@@ -184,6 +194,12 @@ impl PhotocraftApp { Ok(asked) } + /// The folder the last save or export dialog was answered with, for any document, when+ /// Preferences ▸ File Handling › Save As to Original Folder is off (#9656).+ pub(crate) fn last_save_dir(&self) -> Option<std::path::PathBuf> {+ Some(self.session.prefs().file_handling.last_save_folder.as_str()).filter(|d| !d.is_empty()).map(std::path::PathBuf::from)+ }+ /// Whether a save to `path` replaces a file the save dialog didn't ask about: the dialog /// answered `answered`, and [`saved_path`] changed it. A file at `path` that /// [`Services::same_file`](crate::Services::same_file) finds is the same entry as `answered` (the@@ -738,6 +738,69 @@ fn save_dialogs_remember_where_each_document_was_last_saved() { app.poll_file_dialog(&ctx, None); } +/// #9656: Save As and the export dialogs start in the open document's folder by default; with+/// Save As to Original Folder off they start in the folder last saved to, for any document.+#[test]+fn save_as_starts_in_the_document_folder_unless_original_folder_is_off() {+ let (mut app, open, _) = app();+ let ctx = egui::Context::default();+ app.session.active_mut().unwrap().path = Some(joined("/clients/a", "design.psd"));+ let suggested = |open: &Open| match &open.borrow()[0].0 {+ FileDialogRequest::Save { suggested } => suggested.clone(),+ other => panic!("save dialog expected, got {other:?}"),+ };+ let ask = |app: &mut PhotocraftApp, open: &Open, id: &str, answer_with: Option<&str>| {+ menus::invoke(app, &ctx, id, json!({})).unwrap();+ app.poll_file_dialog(&ctx, None);+ let asked = suggested(open);+ answer(open, answer_with.map(|p| FileDialogAnswer::SaveTo(p.into())));+ app.poll_file_dialog(&ctx, None);+ asked+ };+ // The default: Save As offers the document's own path, and nothing is remembered across documents.+ assert!(app.session.prefs().file_handling.save_as_to_original_folder);+ assert_eq!(ask(&mut app, &open, "file.saveAs", Some(&joined("/exports", "design.psd"))), joined("/clients/a", "design.psd"));+ assert_eq!(app.session.prefs().file_handling.last_save_folder, Path::new("/exports").to_string_lossy());+ app.session.active_mut().unwrap().path = Some(joined("/clients/a", "design.psd"));+ assert_eq!(ask(&mut app, &open, "file.saveAs", None), joined("/clients/a", "design.psd"));+ // Off: the name stays, the folder is the one last saved to.+ app.run("prefs.set", json!({"path": "fileHandling.saveAsToOriginalFolder", "value": false})).unwrap();+ assert_eq!(ask(&mut app, &open, "file.saveAs", None), joined("/exports", "design.psd"));+ // A bare export name follows the same rule, for a document that was never saved there.+ app.pick_save("result.png", |_, _| Ok(Value::Null)).unwrap();+ app.poll_file_dialog(&ctx, None);+ assert_eq!(suggested(&open), joined("/exports", "result.png"));+ answer(&open, None);+ app.poll_file_dialog(&ctx, None);+ // Nothing remembered yet: fall back to the document's folder.+ app.run("prefs.set", json!({"path": "fileHandling.lastSaveFolder", "value": ""})).unwrap();+ assert_eq!(ask(&mut app, &open, "file.saveAs", None), joined("/clients/a", "design.psd"));+ app.pick_save("result.png", |_, _| Ok(Value::Null)).unwrap();+ app.poll_file_dialog(&ctx, None);+ assert_eq!(suggested(&open), joined("/clients/a", "result.png"));+ answer(&open, None);+ app.poll_file_dialog(&ctx, None);+}++/// #9656: with Save As to Original Folder off, File › Save of a flat file that asks for a name+/// still offers the document's own file; only Save As moves to the remembered folder.+#[test]+fn plain_save_keeps_the_documents_file_when_original_folder_is_off() {+ let ctx = egui::Context::default();+ let path = "/pics/My image.jpg";+ let (mut app, open, _) = jpeg_app(None, false);+ app.opened_from(path);+ app.run("image.adjustments.invert", json!({})).unwrap();+ app.run("prefs.set", json!({"values": {"fileHandling.saveAsToOriginalFolder": false, "fileHandling.lastSaveFolder": "/exports"}})).unwrap();+ for (id, expected) in [("file.save", path.to_string()), ("file.saveAs", joined("/exports", "My image.jpg"))] {+ menus::invoke(&mut app, &ctx, id, json!({})).unwrap();+ app.poll_file_dialog(&ctx, None);+ assert!(matches!(&open.borrow()[0].0, FileDialogRequest::Save { suggested } if *suggested == expected), "{id}");+ answer(&open, None);+ app.poll_file_dialog(&ctx, None);+ }+}+ #[test] fn save_suggestions_preserve_explicit_directories_and_untitled_names() { let (mut app, open, _) = app();@@ -3911,6 +3911,7 @@ navigator Zoom Zvětšení Chisel Soft Měkké tesání Open color-less EXR as ACEScg Otevřít EXR bez údaje o barevném prostoru jako ACEScg Do not append "copy" to filename when saving a copy Nepřidávat „copy“ k názvu souboru při ukládání kopie+ Save As to Original Folder Uložit jako do původní složky # View › Guides › right-click a guide Edit Selected Guides Upravit vybraná vodítka@@ -3315,6 +3315,7 @@ curves Smooth Glätten Chisel Soft Meißeln weich Open color-less EXR as ACEScg Farbangabenloses EXR als ACEScg öffnen Do not append "copy" to filename when saving a copy Beim Speichern einer Kopie „copy“ nicht an den Dateinamen anhängen+ Save As to Original Folder „Speichern unter“ im Originalordner # Export As metadata and profile options Color Profile Farbprofil EXIF EXIF@@ -3997,6 +3997,7 @@ curves Smooth Εξομάλυνση Chisel Soft Μαλακή σμίλευση Open color-less EXR as ACEScg Άνοιγμα EXR χωρίς χρωματική δήλωση ως ACEScg Do not append "copy" to filename when saving a copy Να μην προστίθεται «copy» στο όνομα αρχείου κατά την αποθήκευση αντιγράφου+ Save As to Original Folder Αποθήκευση ως στον αρχικό φάκελο # Export As metadata and profile options EXIF EXIF @@ -4260,6 +4260,7 @@ navigator Zoom Zoom Chisel Soft Cincelar suave Open color-less EXR as ACEScg Abrir EXR sin espacio de color como ACEScg Do not append "copy" to filename when saving a copy No añadir «copy» al nombre de archivo al guardar una copia+ Save As to Original Folder Guardar como en la carpeta original # Export As metadata and profile options # View › Guides › right-click a guide@@ -3307,6 +3307,7 @@ curves Smooth Lisser Chisel Soft Ciselage doux Open color-less EXR as ACEScg Ouvrir les EXR sans espace colorimétrique en ACEScg Do not append "copy" to filename when saving a copy Ne pas ajouter « copy » au nom de fichier lors de l’enregistrement d’une copie+ Save As to Original Folder Enregistrer sous dans le dossier d’origine # View › Guides › right-click a guide Edit Selected Guides Modifier les repères sélectionnés@@ -4190,6 +4190,7 @@ cameraRaw Yellow Sárga Chisel Soft Lágy vésés Open color-less EXR as ACEScg Színinformáció nélküli EXR megnyitása ACEScg-ként Do not append "copy" to filename when saving a copy Ne fűzze a „copy” szót a fájlnévhez másolat mentésekor+ Save As to Original Folder Mentés másként az eredeti mappába # View › Guides › right-click a guide Edit Selected Guides Kijelölt segédvonalak szerkesztése@@ -3310,6 +3310,7 @@ curves Smooth Haluskan Chisel Soft Pahat Lembut Open color-less EXR as ACEScg Buka EXR tanpa informasi warna sebagai ACEScg Do not append "copy" to filename when saving a copy Jangan tambahkan "copy" ke nama file saat menyimpan salinan+ Save As to Original Folder Simpan Sebagai ke Folder Asli # View › Guides › right-click a guide Edit Selected Guides Edit Panduan Terpilih@@ -4262,6 +4262,7 @@ navigator Zoom Zoom Chisel Soft Cesello morbido Open color-less EXR as ACEScg Apri gli EXR senza spazio colore come ACEScg Do not append "copy" to filename when saving a copy Non aggiungere "copy" al nome del file quando si salva una copia+ Save As to Original Folder Salva con nome nella cartella originale # Color management, Liquify, glyph group, warp and Camera Raw labels the Serbian, Vietnamese and Japanese catalogs added Adobe-rgb-compat Adobe RGB (compatibile)@@ -3977,6 +3977,7 @@ curves Smooth スムーズ Chisel Soft チゼルソフト Open color-less EXR as ACEScg 色空間のないEXRをACEScgとして開く Do not append "copy" to filename when saving a copy コピーを保存するときにファイル名に「copy」を付けない+ Save As to Original Folder 「別名で保存」を元のフォルダーに # View › Guides › right-click a guide Edit Selected Guides 選択したガイドを編集 Clear Selected Guides 選択したガイドを消去@@ -3502,6 +3502,7 @@ curves Smooth 매끄럽게 Chisel Soft 부드럽게 조각 Open color-less EXR as ACEScg 색 공간 정보 없는 EXR을 ACEScg로 열기 Do not append "copy" to filename when saving a copy 사본 저장 시 파일 이름에 "copy"를 붙이지 않음+ Save As to Original Folder 다른 이름으로 저장 시 원본 폴더 사용 # Export As metadata and profile options Color Profile 색상 프로필 EXIF EXIF@@ -4059,6 +4059,7 @@ curves Smooth Vloeiend maken Chisel Soft Beitel zacht Open color-less EXR as ACEScg EXR zonder kleurruimte als ACEScg openen Do not append "copy" to filename when saving a copy Geen "copy" aan de bestandsnaam toevoegen bij het opslaan van een kopie+ Save As to Original Folder Opslaan als in oorspronkelijke map # View › Guides › right-click a guide Edit Selected Guides Geselecteerde hulplijnen bewerken@@ -3914,6 +3914,7 @@ curves Smooth Wygładź Chisel Soft Dłuto miękkie Open color-less EXR as ACEScg Otwieraj EXR bez profilu koloru jako ACEScg Do not append "copy" to filename when saving a copy Nie dodawaj „copy” do nazwy pliku przy zapisywaniu kopii+ Save As to Original Folder Zapisz jako w folderze oryginału # View › Guides › right-click a guide Edit Selected Guides Edytuj zaznaczone prowadnice@@ -4089,6 +4089,7 @@ layerLabel No Color Sem cor Chisel Soft Cinzel suave Open color-less EXR as ACEScg Abrir EXR sem espaço de cor como ACEScg Do not append "copy" to filename when saving a copy Não acrescentar "copy" ao nome do arquivo ao salvar uma cópia+ Save As to Original Folder Salvar como na pasta original # View › Guides › right-click a guide Edit Selected Guides Editar guias selecionadas@@ -3273,6 +3273,7 @@ curves Smooth Сгладить Chisel Soft Мягкая огранка Open color-less EXR as ACEScg Открывать EXR без цветового профиля как ACEScg Do not append "copy" to filename when saving a copy Не добавлять «copy» к имени файла при сохранении копии+ Save As to Original Folder «Сохранить как» в исходную папку # View › Guides › right-click a guide Edit Selected Guides Изменить выбранные направляющие@@ -4330,6 +4330,7 @@ shape Appearance Izgled Chisel Soft Meko klesanje Open color-less EXR as ACEScg Otvori EXR bez podatka o bojama kao ACEScg Do not append "copy" to filename when saving a copy Ne dodaji „copy“ u ime fajla pri čuvanju kopije+ Save As to Original Folder Sačuvaj kao u originalnu fasciklu Show home recent files Prikaži nedavne datoteke na početnom ekranu # View › Guides › right-click a guide@@ -4002,6 +4002,7 @@ cameraData Lens Lens Chisel Soft Yumuşak Keski Open color-less EXR as ACEScg Renk bilgisi olmayan EXR'yi ACEScg olarak aç Do not append "copy" to filename when saving a copy Kopya kaydederken dosya adına "copy" ekleme+ Save As to Original Folder Farklı Kaydet’i özgün klasörde başlat # View › Guides › right-click a guide Edit Selected Guides Seçili Kılavuzları Düzenle@@ -3999,6 +3999,7 @@ curves Smooth Згладити Chisel Soft М'яке різьблення Open color-less EXR as ACEScg Відкривати EXR без колірного профілю як ACEScg Do not append "copy" to filename when saving a copy Не додавати «copy» до імені файлу під час збереження копії+ Save As to Original Folder «Зберегти як» в початкову папку # View › Guides › right-click a guide Edit Selected Guides Редагувати вибрані напрямні@@ -3762,6 +3762,7 @@ curves Smooth Làm mượt Chisel Soft Đục mềm Open color-less EXR as ACEScg Mở EXR không khai báo màu sắc dưới dạng ACEScg Do not append "copy" to filename when saving a copy Không thêm "copy" vào tên tệp khi lưu bản sao+ Save As to Original Folder Lưu dưới dạng vào thư mục gốc # View › Guides › right-click a guide Edit Selected Guides Sửa đường căn đã chọn@@ -2950,6 +2950,7 @@ curves Smooth 平滑 Chisel Soft 雕刻柔和 Open color-less EXR as ACEScg 将无色彩空间信息的 EXR 以 ACEScg 打开 Do not append "copy" to filename when saving a copy 存储副本时不在文件名后添加“copy”+ Save As to Original Folder “存储为”到原始文件夹 # Export As metadata and profile options Color Profile 颜色配置文件 EXIF EXIF@@ -2914,6 +2914,7 @@ curves Smooth 平滑 Chisel Soft 雕刻柔和 Open color-less EXR as ACEScg 將無色彩空間資訊的 EXR 以 ACEScg 開啟 Do not append "copy" to filename when saving a copy 儲存副本時不在檔名後加上「copy」+ Save As to Original Folder 「另存新檔」至原始資料夾 # Export As metadata and profile options Color Profile 色彩描述檔 EXIF EXIF@@ -60,6 +60,9 @@ pub(super) fn humanize(key: &str) -> String { // Performance: presentation without vsync (#1343); like the other graphics settings, it // takes effect when the window next opens. // File Handling: Photoshop's wording (#8080).+ if key == "saveAsToOriginalFolder" {+ return "Save As to Original Folder".into();+ } if key == "doNotAppendCopy" { return "Do not append \"copy\" to filename when saving a copy".into(); }@@ -351,6 +354,8 @@ pub(super) fn field_visible(section: &str, obj: &Map<String, Value>, k: &str, qu let drawn_elsewhere = (section == "interface" && matches!(k, "theme" | "appearanceMode" | "darkTheme" | "lightTheme" | "favoriteThemes")) // View › Rulers (⌘R) sets it, as in Photoshop. || path == "unitsAndRulers.showRulers"+ // The folder of the last save, kept for Save As to Original Folder (#9656).+ || path == "fileHandling.lastSaveFolder" || (section == "performance" && matches!(k, "useGpu" | "gpuBackend" | "gpuAdapter" | "renderingMode" | "memoryUsageMb")) // The browser presents frames itself: Low-Latency Drawing does nothing there. || (cfg!(target_arch = "wasm32") && path == "performance.lowLatencyDrawing")@@ -942,6 +942,21 @@ fn ui_font_size_commands_follow_themes_and_dpi_and_reset_live() { assert!((ctx.pixels_per_point() - 1.5).abs() < 1e-4, "resetting font size keeps UI Scale"); } +#[test]+fn the_last_save_folder_survives_a_restart() {+ let (mut app, store) = app_with_store();+ let ctx = egui::Context::default();+ tick(&mut app, &ctx);+ app.run("prefs.set", json!({"values": {"fileHandling.saveAsToOriginalFolder": false}})).unwrap();+ app.session.prefs.edit(|p| p.file_handling.last_save_folder = "/exports".into());+ tick(&mut app, &ctx);+ let saved = store.lock().unwrap().clone().unwrap();+ let (mut app2, _) = app_with_saved(Some(saved));+ tick(&mut app2, &ctx);+ assert_eq!(app2.session.prefs().file_handling.last_save_folder, "/exports");+ assert_eq!(app2.last_save_dir(), Some(std::path::PathBuf::from("/exports")));+}+ #[test] fn recent_files_survive_a_restart_and_honour_the_count() { let (mut app, store) = app_with_store();@@ -9,7 +9,7 @@ Generated by `cargo xtask scorecard` from `perf/budgets.toml`, `perf/baseline.js | [Performance](#performance) | 3 | 22 | 31 | 65 scenarios: 3 within budget, 22 over budget or failing, 9 tracked only, 27 not run, 4 not measurable yet (baseline 2026-10-05) | | [File compatibility](#file-compatibility) | 11 | 14 | 2 | io corpus: oracle ≥ 148/170, round trip ≥ 169/170; psd-tools corpus: oracle ≥ 249/309, round trip ≥ 308/309; photoshop oracles: oracle ≥ 190/258, round trip ≥ 258/258 | | [Tools](#tools) | 36 | 9 | 0 | |-| [Workspace and UI](#workspace-and-ui) | 15 | 6 | 2 | settings that do nothing: 12 of 178 (target 0) |+| [Workspace and UI](#workspace-and-ui) | 15 | 6 | 2 | settings that do nothing: 12 of 180 (target 0) | | [Type](#type) | 5 | 5 | 0 | | | [Automation](#automation) | 5 | 2 | 4 | 631 of 631 menu items live (100.0%) | | [Reliability](#reliability) | 5 | 2 | 0 | never-crash lints on 30 of 30 crates; panic_hunt present |@@ -187,7 +187,7 @@ Corpus floors (`crates/io/tests/it/corpus.rs`): the enforced minimums; the corpu ## Workspace and UI -**Settings that do nothing: 12 of 178** (target 0, #204). Counted by the prefs audit (`xtask/src/scorecard.rs`): a `Preferences` field counts as read when code outside the prefs model (`prefs.rs` except its `impl Session` blocks; tests excluded; the schema-driven dialog never names a field) accesses `section.field`, or `.section` and `.field` in the same file, or calls a section method that reads `self.field`. It is a heuristic lower bound: a generic name can look read when it isn't.+**Settings that do nothing: 12 of 180** (target 0, #204). Counted by the prefs audit (`xtask/src/scorecard.rs`): a `Preferences` field counts as read when code outside the prefs model (`prefs.rs` except its `impl Session` blocks; tests excluded; the schema-driven dialog never names a field) accesses `section.field`, or `.section` and `.field` in the same file, or calls a section method that reads `self.field`. It is a heuristic lower bound: a generic name can look read when it isn't. Unread: `general.color_picker`, `workspace.auto_show_hidden_panels`, `workspace.enable_floating_document_window_docking`, `performance.gpu_adapter`, `performance.legacy_compositing`, `scratch_disks.disks`, `units_and_rulers.column_width`, `units_and_rulers.gutter`, `plug_ins.show_extension_panels`, `plug_ins.allow_scripts_to_connect`, `type_.text_engine`, `enhanced_controls.scrubby_slider_acceleration`.