Type: wire missingGlyphProtection (script fallback toggle) (#3863)

* text: missing glyph protection can turn the script fallback stack off (#1909)

* engine: apply type.missingGlyphProtection to the live text engine

* docs: scorecard, settings that do nothing 30 -> 29 of 154
This commit is contained in:
Reynold Hu 2026-10-12 20:34:22 +08:00 committed by GitHub
parent 25c0ec78d5
commit b0554b8178
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 53 additions and 7 deletions

View File

@ -869,6 +869,15 @@ impl Default for TypePrefs {
}
}
impl TypePrefs {
/// Preferences ▸ Type ▸ Missing Glyph Protection: with it on (the default, as in Photoshop),
/// glyphs the chosen face lacks fall back through the coverage fonts (#1909). The text engine
/// applies this in `Session::apply_prefs`.
pub fn missing_glyph_protection(&self) -> bool {
self.missing_glyph_protection
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct EnhancedControls {
@ -1049,7 +1058,6 @@ pub const HIDDEN_UNTIL_IMPLEMENTED: &[&str] = &[
"plugIns.showExtensionPanels",
"plugIns.allowScriptsToConnect",
"plugIns.generatorEnabled",
"type.missingGlyphProtection",
"type.textEngine",
"type.recentFonts",
"enhancedControls.scrubbySliderAcceleration",
@ -1484,8 +1492,8 @@ impl Session {
r
}
/// Push engine-relevant preferences into live state: every document's history limit and
/// the layer-effect cache budget.
/// Push engine-relevant preferences into live state: every document's history limit, the
/// layer-effect cache budget and the text engine's missing-glyph protection.
pub fn apply_prefs(&mut self) {
let n = self.prefs().performance.history_states.max(1) as usize;
let bytes = self.prefs().performance.history_budget_bytes();
@ -1496,6 +1504,9 @@ impl Session {
st.history.trim(&st.doc);
}
photocraft_compose::set_effect_cache_budget(budget << 20);
if let Ok(mut engine) = photocraft_text::shared().lock() {
engine.fonts.set_missing_glyph_protection(self.prefs().type_.missing_glyph_protection());
}
crate::plugin_cmds::sync_prefs(self);
}

View File

@ -140,6 +140,9 @@ pub struct FontDb {
/// PostScript name → (family, weight, italic), filled lazily.
ps_cache: HashMap<String, Option<ResolvedFont>>,
fallbacks: Vec<String>,
/// Preferences ▸ Type ▸ Missing Glyph Protection: route glyphs the chosen face lacks through
/// [`Self::fallback_stack`] (#1909). Off, only the default family shapes the text.
missing_glyph_protection: bool,
face_cache: HashMap<String, Vec<FaceInfo>>,
face_aliases: HashMap<(String, usize), String>,
}
@ -159,6 +162,7 @@ impl FontDb {
system_loaded: false,
ps_cache: HashMap::new(),
fallbacks: Vec::new(),
missing_glyph_protection: true,
face_cache: HashMap::new(),
face_aliases: HashMap::new(),
};
@ -272,8 +276,20 @@ impl FontDb {
}
/// Families available after the requested one (bundled default + installed coverage fonts).
/// With missing glyph protection off (Preferences ▸ Type), only the default family shapes.
pub(crate) fn fallback_stack(&self) -> impl Iterator<Item = &str> {
std::iter::once(DEFAULT_FAMILY).chain(self.fallbacks.iter().map(String::as_str))
let fallbacks = self.missing_glyph_protection.then_some(self.fallbacks.iter().map(String::as_str));
std::iter::once(DEFAULT_FAMILY).chain(fallbacks.into_iter().flatten())
}
/// Preferences ▸ Type ▸ Missing Glyph Protection, applied by the engine's `apply_prefs`.
pub fn set_missing_glyph_protection(&mut self, on: bool) {
self.missing_glyph_protection = on;
}
/// Is missing glyph protection on?
pub fn missing_glyph_protection(&self) -> bool {
self.missing_glyph_protection
}
/// All family names, sorted.

View File

@ -1580,6 +1580,25 @@ fn a_served_script_fallback_joins_the_fallback_stack_once_it_arrives() {
assert!(db.fallback_stack().any(|f| f == "Cairo"), "{:?}", db.fallback_stack().collect::<Vec<_>>());
}
/// Preferences ▸ Type ▸ Missing Glyph Protection: off, only the default family is in the
/// fallback stack, so a face without the glyph shows the missing-glyph box instead of a
/// fallback's glyph (#1909).
#[test]
fn missing_glyph_protection_can_turn_the_fallback_stack_off() {
let Some(cairo) = crate::CRAFT_FONTS.iter().find(|f| f.family == "Cairo") else {
eprintln!("skipping: built without craft-fonts (set CRAFT_FONTS_DIR to a craft-fonts checkout)");
return;
};
let mut db = fonts::FontDb::new();
db.register_font_data(cairo.bytes.to_vec());
assert!(db.missing_glyph_protection(), "on by default, as in Photoshop");
assert!(db.fallback_stack().any(|f| f == "Cairo"));
db.set_missing_glyph_protection(false);
assert_eq!(db.fallback_stack().collect::<Vec<_>>(), [fonts::DEFAULT_FAMILY], "only the default family remains");
db.set_missing_glyph_protection(true);
assert!(db.fallback_stack().any(|f| f == "Cairo"), "turning it back on restores the coverage fonts");
}
#[test]
fn translate_tysh_moves_only_the_translation() {
let mut t = point("Hello!", 26.0);

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, 17 not run, 5 not measurable yet (baseline 2026-10-05) |
| [File compatibility](#file-compatibility) | 6 | 13 | 8 | 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) | 28 | 14 | 1 | |
| [Workspace and UI](#workspace-and-ui) | 13 | 5 | 5 | settings that do nothing: 30 of 155 (target 0) |
| [Workspace and UI](#workspace-and-ui) | 13 | 5 | 5 | settings that do nothing: 29 of 155 (target 0) |
| [Type](#type) | 4 | 2 | 3 | |
| [Automation](#automation) | 4 | 2 | 5 | 631 of 631 menu items live (100.0%) |
| [Reliability](#reliability) | 5 | 2 | 0 | never-crash lints on 28 of 28 crates; panic_hunt present |
@ -176,9 +176,9 @@ Corpus floors (`crates/io/tests/corpus.rs`): the enforced minimums; the corpus t
## Workspace and UI
**Settings that do nothing: 30 of 155** (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: 29 of 155** (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.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`, `file_handling.save_in_background`, `file_handling.maximize_psd_compatibility`, `performance.cache_levels`, `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`, `plug_ins.generator_enabled`, `type_.missing_glyph_protection`, `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`.
Unread: `general.color_picker`, `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`, `file_handling.save_in_background`, `file_handling.maximize_psd_compatibility`, `performance.cache_levels`, `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`, `plug_ins.generator_enabled`, `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 |
|---|---|---|---|---|