From 67e3d9be9f2a0eef9ccd90def9e5d69dca167843 Mon Sep 17 00:00:00 2001 From: Reynold Hu Date: Sun, 11 Oct 2026 16:52:17 +0800 Subject: [PATCH] feat(ui): Font Preview Size drives the font menu samples (#204) (#2668) Co-authored-by: Brandon Thomas --- crates/engine/src/prefs.rs | 1 - crates/ui-egui/src/font_preview.rs | 92 +++++++++++++++----- crates/ui-egui/src/type_font_picker_tests.rs | 28 +++++- crates/ui-egui/src/type_panels_ui.rs | 10 ++- crates/ui-egui/src/type_tool.rs | 41 +++++---- docs/scorecard.md | 6 +- 6 files changed, 129 insertions(+), 49 deletions(-) diff --git a/crates/engine/src/prefs.rs b/crates/engine/src/prefs.rs index a10260b5..20f84c23 100644 --- a/crates/engine/src/prefs.rs +++ b/crates/engine/src/prefs.rs @@ -1007,7 +1007,6 @@ pub const HIDDEN_UNTIL_IMPLEMENTED: &[&str] = &[ "type.missingGlyphProtection", "type.showFontNamesInEnglish", "type.textEngine", - "type.fontPreview", "type.recentFonts", "enhancedControls.scrubbySliderAcceleration", "enhancedControls.touchGestures", diff --git a/crates/ui-egui/src/font_preview.rs b/crates/ui-egui/src/font_preview.rs index 417b7fd8..1568abf6 100644 --- a/crates/ui-egui/src/font_preview.rs +++ b/crates/ui-egui/src/font_preview.rs @@ -6,6 +6,7 @@ use std::sync::PoisonError; use egui::{ColorImage, Rect, TextureHandle, TextureOptions, pos2, vec2}; use photocraft_color::{ColorMode, PixelFormat, SampleType}; use photocraft_doc::TextLayer; +use photocraft_engine::prefs::FontPreview; use photocraft_geom::Affine; const SAMPLE: &str = "AaBbCc"; @@ -13,16 +14,44 @@ const SAMPLE: &str = "AaBbCc"; const ARABIC_CORE: [char; 4] = ['\u{627}', '\u{644}', '\u{645}', '\u{639}']; /// Short, recognisable, and joins letters, so it shows the face's shaping. const ARABIC_SAMPLE: &str = "أبجد هوز"; +/// The Latin and Arabic sample widths and the sample height at the menu's medium (18 pt). const WIDTH: f32 = 100.0; const ARABIC_WIDTH: f32 = 72.0; const GAP: f32 = 6.0; const HEIGHT: f32 = 24.0; +/// The medium point size the widths above are measured at. +const MEDIUM: f32 = 18.0; const CACHE_LIMIT: usize = 128; const CACHE_ID: &str = "font-menu-previews"; -/// Family, display scale, and the font database generations, so a family installed or fetched -/// (served fonts) after its first miss gets a sample once it is available. -type Key = (String, u32, (u64, u64)); +/// A width or height measured at 18 pt, scaled to the sample's point size. +fn scaled(v: f32, size: f32) -> f32 { + (v * size / MEDIUM).round() +} + +/// The Latin sample's `[width, height]` box for a point size: [`WIDTH`]×[`HEIGHT`] at 18 pt (the +/// menu's medium), proportional otherwise. +fn box_for(size: f32) -> [f32; 2] { + [scaled(WIDTH, size), scaled(HEIGHT, size)] +} + +/// The font menu's sample geometry for Preferences ▸ Type ▸ Font Preview Size: the sample's point +/// size and its Latin box; `None` when the preview is off. +pub(crate) fn sample(preview: FontPreview) -> Option<(f32, [f32; 2])> { + let size = match preview { + FontPreview::Off => return None, + FontPreview::Small => 14.0, + FontPreview::Medium => MEDIUM, + FontPreview::Large => 24.0, + FontPreview::ExtraLarge => 32.0, + FontPreview::Huge => 40.0, + }; + Some((size, box_for(size))) +} + +/// Family, display scale, the font database generations (so a family installed or fetched +/// (served fonts) after its first miss gets a sample once it is available) and the point size. +type Key = (String, u32, (u64, u64), u32); #[derive(Clone)] struct Samples { latin: TextureHandle, @@ -48,15 +77,15 @@ fn sample_texts(chars: &[char]) -> (String, Option<&'static str>) { (SAMPLE.into(), arabic.then_some(ARABIC_SAMPLE)) } -fn render(engine: &mut photocraft_text::TextEngine, family: &str, text: &str, slot: f32, scale: u32) -> Option { +fn render(engine: &mut photocraft_text::TextEngine, family: &str, text: &str, slot: f32, scale: u32, size: f32) -> Option { if text.is_empty() { return None; } - let layer = TextLayer { text: text.into(), font_family: family.into(), size_pt: 18.0 * scale as f32, ..Default::default() }; + let layer = TextLayer { text: text.into(), font_family: family.into(), size_pt: size * scale as f32, ..Default::default() }; let layout = engine.layout(&layer, 72.0); let bounds = layout.bounds()?; let width = (slot as u32 * scale) as usize; - let height = (HEIGHT as u32 * scale) as usize; + let height = (scaled(HEIGHT, size) as u32 * scale) as usize; let transform = Affine::translate(f64::from(-bounds[0]), f64::from((height as f32 - (bounds[3] - bounds[1])) / 2.0 - bounds[1])); // Reject oversized ink before the rasterizer allocates a surface (malformed installed fonts). let ink = photocraft_text::render::ink_rect(&layout, &transform); @@ -69,38 +98,38 @@ fn render(engine: &mut photocraft_text::TextEngine, family: &str, text: &str, sl Some(ColorImage::from_rgba_unmultiplied([width, height], &rgba.into_iter().flat_map(|p| [255, 255, 255, p[3]]).collect::>())) } -fn images(engine: &mut photocraft_text::TextEngine, family: &str, scale: u32) -> Option { +fn images(engine: &mut photocraft_text::TextEngine, family: &str, scale: u32, size: f32) -> Option { // Unknown/unreadable families keep their plain menu label, rather than a misleading fallback. if !engine.fonts.has_family(family) { return None; } let (latin, arabic) = sample_texts(&engine.fonts.charmap(family, 400, false)); - let latin = render(engine, family, &latin, WIDTH, scale)?; - let arabic = arabic.and_then(|text| render(engine, family, text, ARABIC_WIDTH, scale)); + let latin = render(engine, family, &latin, scaled(WIDTH, size), scale, size)?; + let arabic = arabic.and_then(|text| render(engine, family, text, scaled(ARABIC_WIDTH, size), scale, size)); Some(SampleImages { latin, arabic }) } -fn key(ctx: &egui::Context, family: &str) -> Key { +fn key(ctx: &egui::Context, family: &str, size: f32) -> Key { let scale = ctx.pixels_per_point().ceil().clamp(1.0, 4.0) as u32; - (family.to_owned(), scale, (photocraft_text::fonts::generation(), photocraft_text::served::generation())) + (family.to_owned(), scale, (photocraft_text::fonts::generation(), photocraft_text::served::generation()), size as u32) } /// Width a row reserves for its samples: wider once a painted row showed the family covers /// Arabic, so the label truncates before the samples. Never renders, so rows scrolled out of /// view cost nothing. -pub(crate) fn slot_width(ctx: &egui::Context, family: &str) -> f32 { - let key = key(ctx, family); +pub(crate) fn slot_width(ctx: &egui::Context, family: &str, size: f32) -> f32 { + let key = key(ctx, family, size); let arabic = ctx.data_mut(|d| { let cache = d.get_temp_mut_or_default::(egui::Id::new(CACHE_ID)); cache.0.iter().any(|(k, samples)| k == &key && samples.as_ref().is_some_and(|s| s.arabic.is_some())) }); - if arabic { WIDTH + GAP + ARABIC_WIDTH } else { WIDTH } + if arabic { scaled(WIDTH, size) + GAP + scaled(ARABIC_WIDTH, size) } else { scaled(WIDTH, size) } } -pub(crate) fn paint(ui: &egui::Ui, family: &str, row: Rect) { +pub(crate) fn paint(ui: &egui::Ui, family: &str, row: Rect, size: f32) { let ctx = ui.ctx(); let scale = ctx.pixels_per_point().ceil().clamp(1.0, 4.0) as u32; - let key = key(ctx, family); + let key = key(ctx, family, size); let id = egui::Id::new(CACHE_ID); let cached = ctx.data_mut(|d| { let cache = d.get_temp_mut_or_default::(id); @@ -111,7 +140,7 @@ pub(crate) fn paint(ui: &egui::Ui, family: &str, row: Rect) { Some(samples) }); let samples = cached.unwrap_or_else(|| { - let samples = images(&mut photocraft_text::shared().lock().unwrap_or_else(PoisonError::into_inner), family, scale).map(|img| { + let samples = images(&mut photocraft_text::shared().lock().unwrap_or_else(PoisonError::into_inner), family, scale, size).map(|img| { let load = |image| ctx.load_texture(format!("font-sample-{family}"), image, TextureOptions::LINEAR); Samples { latin: load(img.latin), arabic: img.arabic.map(load) } }); @@ -131,10 +160,12 @@ pub(crate) fn paint(ui: &egui::Ui, family: &str, row: Rect) { if let Some(samples) = samples { let tint = crate::theme::Tokens::get(ctx).text; let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)); - let latin = Rect::from_min_size(pos2(row.right() - WIDTH - 8.0, row.center().y - HEIGHT / 2.0), vec2(WIDTH, HEIGHT)); + let [w, h] = box_for(size); + let latin = Rect::from_min_size(pos2(row.right() - w - 8.0, row.center().y - h / 2.0), vec2(w, h)); ui.painter().image(samples.latin.id(), latin, uv, tint); if let Some(arabic) = samples.arabic { - let rect = Rect::from_min_size(pos2(latin.left() - GAP - ARABIC_WIDTH, latin.top()), vec2(ARABIC_WIDTH, HEIGHT)); + let aw = scaled(ARABIC_WIDTH, size); + let rect = Rect::from_min_size(pos2(latin.left() - GAP - aw, latin.top()), vec2(aw, h)); ui.painter().image(arabic.id(), rect, uv, tint); } } @@ -145,7 +176,7 @@ mod tests { use super::*; fn image(engine: &mut photocraft_text::TextEngine, family: &str, scale: u32) -> Option { - images(engine, family, scale).map(|i| i.latin) + images(engine, family, scale, MEDIUM).map(|i| i.latin) } fn charmap(text: &str) -> Vec { @@ -166,14 +197,27 @@ mod tests { assert!(image(&mut engine, "Missing font", 1).is_none()); } + /// Preferences ▸ Type ▸ Font Preview Size: Small…Huge scale the sample, Off removes it. + #[test] + fn the_preview_size_preference_scales_the_sample() { + assert_eq!(sample(FontPreview::Off), None); + assert_eq!(sample(FontPreview::Medium), Some((18.0, [100.0, 24.0]))); + let Some((_, [w, h])) = sample(FontPreview::Huge) else { panic!("Huge previews") }; + assert!(w > 100.0 && h > 24.0, "Huge is bigger than Medium: {w}×{h}"); + assert!(sample(FontPreview::Small).unwrap().1[0] < sample(FontPreview::ExtraLarge).unwrap().1[0], "sizes are ordered"); + let mut engine = photocraft_text::TextEngine::new(); + let big = images(&mut engine, "Inter", 1, 32.0).unwrap().latin; + assert_eq!(big.size, [178, 43], "the box follows the size"); + } + #[test] fn latin_only_families_get_no_arabic_sample() { let mut engine = photocraft_text::TextEngine::new(); for family in ["Inter", "JetBrains Mono"] { - assert!(images(&mut engine, family, 1).expect("bundled").arabic.is_none(), "{family}"); + assert!(images(&mut engine, family, 1, MEDIUM).expect("bundled").arabic.is_none(), "{family}"); } - assert!(images(&mut engine, "", 1).is_none()); - assert!(images(&mut engine, "Missing font", 1).is_none()); + assert!(images(&mut engine, "", 1, MEDIUM).is_none()); + assert!(images(&mut engine, "Missing font", 1, MEDIUM).is_none()); } #[test] @@ -202,7 +246,7 @@ mod tests { eprintln!("skipped: no installed family covers both Latin and Arabic"); return; }; - let images = images(&mut engine, &family, 1).expect(&family); + let images = images(&mut engine, &family, 1, MEDIUM).expect(&family); let arabic = images.arabic.expect("Arabic sample"); assert_eq!(arabic.size, [ARABIC_WIDTH as usize, HEIGHT as usize]); assert!(arabic.pixels.iter().any(|p| p.a() > 0), "{family}"); diff --git a/crates/ui-egui/src/type_font_picker_tests.rs b/crates/ui-egui/src/type_font_picker_tests.rs index 137a4a50..92e10fbf 100644 --- a/crates/ui-egui/src/type_font_picker_tests.rs +++ b/crates/ui-egui/src/type_font_picker_tests.rs @@ -5,23 +5,29 @@ use egui::accesskit::Role; use egui::{Key, Modifiers, Rect, vec2}; use egui_kittest::Harness; use egui_kittest::kittest::Queryable; +use photocraft_engine::prefs::FontPreview; struct State { current: String, families: Vec, popup: egui::Id, + preview: FontPreview, } fn harness() -> Harness<'static, State> { + harness_with(FontPreview::Medium) +} + +fn harness_with(preview: FontPreview) -> Harness<'static, State> { // Enough families that the unfiltered menu hits its maximum height and scrolls. let mut families: Vec = (0..60).map(|i| format!("Family {i:02}")).collect(); families.push("Zeta Sans".into()); - let state = State { current: "Family 00".into(), families, popup: egui::Id::NULL }; + let state = State { current: "Family 00".into(), families, popup: egui::Id::NULL, preview }; let mut h = Harness::builder().with_size(vec2(600.0, 900.0)).build_ui_state( |ui, s: &mut State| { s.popup = ui.make_persistent_id(egui::IdSalt::new("type-font")).with("popup"); let fams = s.families.clone(); - font_picker_in(ui, &mut s.current, 170.0, &fams); + font_picker_in(ui, &mut s.current, 170.0, &fams, s.preview); }, state, ); @@ -376,3 +382,21 @@ fn hovering_a_font_previews_only_the_selected_characters() { assert_eq!(fonts(&preview), ["JetBrains Mono", "JetBrains Mono", "Inter", "Inter", "Inter", "Inter"]); assert_eq!(fonts(&app.session.active().unwrap().doc), ["JetBrains Mono", "JetBrains Mono", "Inter", "Inter", "Inter", "Inter"]); } + +/// Preferences ▸ Type ▸ Font Preview Size: Extra Large grows the sample rows, Off drops the +/// samples (rows shrink to the plain height). +#[test] +fn the_preview_size_preference_resizes_or_removes_the_samples() { + let mut h = harness_with(FontPreview::Medium); + open(&mut h); + let medium = h.get_by_label("Family 00").rect().height(); + assert!((medium - 28.0).abs() < 0.5, "medium rows hold the 24 px sample: {medium}"); + let mut h = harness_with(FontPreview::ExtraLarge); + open(&mut h); + let extra = h.get_by_label("Family 00").rect().height(); + assert!(extra > medium, "extra large rows are taller: {extra} vs {medium}"); + let mut h = harness_with(FontPreview::Off); + open(&mut h); + let off = h.get_by_label("Family 00").rect().height(); + assert!((off - 28.0).abs() < 0.5, "off keeps the plain row height: {off}"); +} diff --git a/crates/ui-egui/src/type_panels_ui.rs b/crates/ui-egui/src/type_panels_ui.rs index 761dbc25..fbab3f84 100644 --- a/crates/ui-egui/src/type_panels_ui.rs +++ b/crates/ui-egui/src/type_panels_ui.rs @@ -371,6 +371,8 @@ fn options_editor(app: &mut PhotocraftApp, ui: &mut egui::Ui, paragraph: bool, s } }); let defined = |attrs: &Value, k: &str| attrs.get(k).is_some(); + // Preferences ▸ Type ▸ Font Preview Size sizes the family picker's samples. + let preview = app.session.prefs().type_.font_preview; // Preferences ▸ Units & Rulers ▸ Type: the model stores points; the fields may show pixels // (by the document's resolution) or millimetres. let (type_unit, unit_label) = crate::type_tool::type_unit(app); @@ -397,7 +399,7 @@ fn options_editor(app: &mut PhotocraftApp, ui: &mut egui::Ui, paragraph: bool, s ui.add_enabled_ui(on, |ui| match kind { "font" => { let mut f = v.as_str().unwrap_or("").to_string(); - if family_picker(ui, &format!("style-font-{paragraph}-{id}"), &mut f) { + if family_picker(ui, &format!("style-font-{paragraph}-{id}"), &mut f, preview) { set = Some(json!({ panel: f })); } } @@ -506,9 +508,9 @@ fn options_editor(app: &mut PhotocraftApp, ui: &mut egui::Ui, paragraph: bool, s } /// Searchable family combo box. -fn family_picker(ui: &mut egui::Ui, salt: &str, current: &mut String) -> bool { +fn family_picker(ui: &mut egui::Ui, salt: &str, current: &mut String, preview: photocraft_engine::prefs::FontPreview) -> bool { let mut shown = if current.is_empty() { photocraft_text::fonts::DEFAULT_FAMILY.to_string() } else { current.clone() }; - let changed = crate::type_tool::family_picker_in(ui, salt, &mut shown, 140.0, &crate::type_tool::families()); + let changed = crate::type_tool::family_picker_in(ui, salt, &mut shown, 140.0, &crate::type_tool::families(), preview); if changed { *current = shown; } @@ -598,7 +600,7 @@ pub fn glyphs_panel(app: &mut PhotocraftApp, ui: &mut egui::Ui) { // Font family + style. ui.horizontal(|ui| { let mut f = family.clone(); - if family_picker(ui, "glyph-family", &mut f) { + if family_picker(ui, "glyph-family", &mut f, app.session.prefs().type_.font_preview) { app.ui.type_panels.glyph_family = f; app.ui.type_panels.glyph_style = tl!("Regular").into(); } diff --git a/crates/ui-egui/src/type_tool.rs b/crates/ui-egui/src/type_tool.rs index 2b5868ea..069bd9d7 100644 --- a/crates/ui-egui/src/type_tool.rs +++ b/crates/ui-egui/src/type_tool.rs @@ -844,8 +844,8 @@ pub fn style_label(style: &str) -> String { } /// Searchable font-family combo box. -fn font_picker(ui: &mut egui::Ui, current: &mut String, width: f32) -> bool { - family_picker_in(ui, "type-font", current, width, &families()) +fn font_picker(ui: &mut egui::Ui, current: &mut String, width: f32, preview: photocraft_engine::prefs::FontPreview) -> bool { + family_picker_in(ui, "type-font", current, width, &families(), preview) } /// Maximum height of the font menu. @@ -853,13 +853,20 @@ const FONT_MENU_HEIGHT: f32 = 460.0; /// [`font_picker`] over a given family list (tests pass their own). #[cfg(test)] -fn font_picker_in(ui: &mut egui::Ui, current: &mut String, width: f32, families: &[String]) -> bool { - family_picker_in(ui, "type-font", current, width, families) +fn font_picker_in(ui: &mut egui::Ui, current: &mut String, width: f32, families: &[String], preview: photocraft_engine::prefs::FontPreview) -> bool { + family_picker_in(ui, "type-font", current, width, families, preview) } /// Shared by the Type options, Character, style and Glyphs panels. Selection changes still /// flow through each caller's existing engine command. -pub(crate) fn family_picker_in(ui: &mut egui::Ui, salt: &str, current: &mut String, width: f32, families: &[String]) -> bool { +pub(crate) fn family_picker_in( + ui: &mut egui::Ui, + salt: &str, + current: &mut String, + width: f32, + families: &[String], + preview: photocraft_engine::prefs::FontPreview, +) -> bool { let mut changed = false; let search_id = ui.id().with(("font-search", salt)); let combo = egui::ComboBox::from_id_salt(salt) @@ -917,20 +924,24 @@ pub(crate) fn family_picker_in(ui: &mut egui::Ui, salt: &str, current: &mut Stri ui.data_mut(|d| d.remove::(search_id)); ui.close(); } + let preview = crate::font_preview::sample(preview); + let row_h = preview.map_or(28.0, |(_, [_, h])| (h + 4.0).max(28.0)); let mut hovered: Option = None; for f in filtered { - let response = ui.add_sized( - [330.0, 28.0], - egui::Button::selectable(f == current, f) - .truncate() - .right_text(egui::Atom::custom(ui.id().with(("font-sample", f)), egui::vec2(crate::font_preview::slot_width(ui.ctx(), f), 24.0))), - ); + let mut button = egui::Button::selectable(f == current, f).truncate(); + if let Some((size, [_, h])) = preview { + button = + button.right_text(egui::Atom::custom(ui.id().with(("font-sample", f)), egui::vec2(crate::font_preview::slot_width(ui.ctx(), f, size), h))); + } + let response = ui.add_sized([330.0, row_h], button); if (down || up) && f == current { response.scroll_to_me(Some(egui::Align::Center)); } - if ui.is_rect_visible(response.rect) { - crate::font_preview::paint(ui, f, response.rect); + if let Some((size, _)) = preview + && ui.is_rect_visible(response.rect) + { + crate::font_preview::paint(ui, f, response.rect, size); } if response.hovered() { hovered = Some(f.clone()); @@ -1095,7 +1106,7 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) { app.ui.status_error = true; } } - if font_picker(ui, &mut fam, 170.0) { + if font_picker(ui, &mut fam, 170.0, app.session.prefs().type_.font_preview) { app.ui.tool_options.type_font = fam.clone(); let st = styles(&fam); style = if st.contains(&style) { style } else { st.first().cloned().unwrap_or_else(|| "Regular".into()) }; @@ -1398,7 +1409,7 @@ fn type_sections(app: &mut PhotocraftApp, ui: &mut egui::Ui, character: bool, pa let w = field_width(full, 2, LABEL_W); let mut fam = c.font_family.clone(); row(ui, &mut |ui| { - if font_picker(ui, &mut fam, full) { + if font_picker(ui, &mut fam, full, app.session.prefs().type_.font_preview) { app.ui.tool_options.type_font = fam.clone(); apply(app, ui.ctx(), json!({"font": fam})); } diff --git a/docs/scorecard.md b/docs/scorecard.md index 4a2d450a..36358372 100644 --- a/docs/scorecard.md +++ b/docs/scorecard.md @@ -9,7 +9,7 @@ Generated by `cargo xtask scorecard` from `perf/budgets.toml`, `perf/baseline.js | [Performance](#performance) | 3 | 22 | 22 | 56 scenarios: 3 within budget, 22 over budget or failing, 9 tracked only, 16 not run, 6 not measurable yet (baseline 2026-10-05) | | [File compatibility](#file-compatibility) | 5 | 11 | 11 | io corpus: oracle ≥ 146/170, round trip ≥ 169/170; psd-tools corpus: oracle ≥ 236/309, round trip ≥ 308/309; photoshop oracles: oracle ≥ 133/256, round trip ≥ 258/256 | | [Tools](#tools) | 21 | 17 | 4 | | -| [Workspace and UI](#workspace-and-ui) | 6 | 7 | 10 | settings that do nothing: 38 of 152 (target 0) | +| [Workspace and UI](#workspace-and-ui) | 6 | 7 | 10 | settings that do nothing: 37 of 152 (target 0) | | [Type](#type) | 0 | 3 | 6 | | | [Automation](#automation) | 3 | 2 | 6 | 630 of 630 menu items live (100.0%) | | [Reliability](#reliability) | 5 | 2 | 0 | never-crash lints on 28 of 28 crates; panic_hunt present | @@ -175,9 +175,9 @@ Corpus floors (`crates/io/tests/corpus.rs`): the enforced minimums; the corpus t ## Workspace and UI -**Settings that do nothing: 38 of 152** (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: 37 of 152** (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`, `general.always_create_smart_objects_when_placing`, `general.animated_zoom`, `general.zoom_resizes_windows`, `workspace.auto_collapse_icon_panels`, `workspace.auto_show_hidden_panels`, `workspace.enable_floating_document_window_docking`, `workspace.enable_narrow_options_bar`, `tools.enable_flick_panning`, `tools.show_transformation_values`, `file_handling.image_previews`, `file_handling.save_in_background`, `file_handling.ignore_exif_profile_tag`, `file_handling.maximize_psd_compatibility`, `performance.cache_levels`, `performance.legacy_compositing`, `scratch_disks.disks`, `units_and_rulers.column_width`, `units_and_rulers.gutter`, `units_and_rulers.print_resolution`, `plug_ins.show_extension_panels`, `plug_ins.allow_scripts_to_connect`, `plug_ins.generator_enabled`, `type_.missing_glyph_protection`, `type_.show_font_names_in_english`, `type_.text_engine`, `type_.font_preview`, `type_.recent_fonts`, `enhanced_controls.scrubby_slider_acceleration`, `enhanced_controls.touch_gestures`, `raw_defaults.color_space`, `raw_defaults.bit_depth`, `raw_defaults.resolution`, `raw_defaults.sharpen_for`, `raw_defaults.open_as_smart_object`, `raw_defaults.apply_auto_tone`, `integrations.allow_agent_control`, `integrations.control_port`. +Unread: `general.color_picker`, `general.always_create_smart_objects_when_placing`, `general.animated_zoom`, `general.zoom_resizes_windows`, `workspace.auto_collapse_icon_panels`, `workspace.auto_show_hidden_panels`, `workspace.enable_floating_document_window_docking`, `workspace.enable_narrow_options_bar`, `tools.enable_flick_panning`, `tools.show_transformation_values`, `file_handling.image_previews`, `file_handling.save_in_background`, `file_handling.ignore_exif_profile_tag`, `file_handling.maximize_psd_compatibility`, `performance.cache_levels`, `performance.legacy_compositing`, `scratch_disks.disks`, `units_and_rulers.column_width`, `units_and_rulers.gutter`, `units_and_rulers.print_resolution`, `plug_ins.show_extension_panels`, `plug_ins.allow_scripts_to_connect`, `plug_ins.generator_enabled`, `type_.missing_glyph_protection`, `type_.show_font_names_in_english`, `type_.text_engine`, `type_.recent_fonts`, `enhanced_controls.scrubby_slider_acceleration`, `enhanced_controls.touch_gestures`, `raw_defaults.color_space`, `raw_defaults.bit_depth`, `raw_defaults.resolution`, `raw_defaults.sharpen_for`, `raw_defaults.open_as_smart_object`, `raw_defaults.apply_auto_tone`, `integrations.allow_agent_control`, `integrations.control_port`. | Id | Target | Status | Issue | Note | |---|---|---|---|---|