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:
parent
25c0ec78d5
commit
b0554b8178
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|---|---|---|---|---|
|
||||
|
|
|
|||
Loading…
Reference in New Issue