feat(ui): Font Preview Size drives the font menu samples (#204) (#2668)

Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Reynold Hu 2026-10-11 16:52:17 +08:00 committed by GitHub
parent 6cfed3a7dc
commit 67e3d9be9f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 129 additions and 49 deletions

View File

@ -1007,7 +1007,6 @@ pub const HIDDEN_UNTIL_IMPLEMENTED: &[&str] = &[
"type.missingGlyphProtection",
"type.showFontNamesInEnglish",
"type.textEngine",
"type.fontPreview",
"type.recentFonts",
"enhancedControls.scrubbySliderAcceleration",
"enhancedControls.touchGestures",

View File

@ -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<ColorImage> {
fn render(engine: &mut photocraft_text::TextEngine, family: &str, text: &str, slot: f32, scale: u32, size: f32) -> Option<ColorImage> {
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::<Vec<_>>()))
}
fn images(engine: &mut photocraft_text::TextEngine, family: &str, scale: u32) -> Option<SampleImages> {
fn images(engine: &mut photocraft_text::TextEngine, family: &str, scale: u32, size: f32) -> Option<SampleImages> {
// 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::<Cache>(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::<Cache>(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<ColorImage> {
images(engine, family, scale).map(|i| i.latin)
images(engine, family, scale, MEDIUM).map(|i| i.latin)
}
fn charmap(text: &str) -> Vec<char> {
@ -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}");

View File

@ -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<String>,
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<String> = (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}");
}

View File

@ -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();
}

View File

@ -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::<String>(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<String> = 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}));
}

View File

@ -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 |
|---|---|---|---|---|