diff --git a/crates/neomacs-layout-engine/src/font/resolver.rs b/crates/neomacs-layout-engine/src/font/resolver.rs index 3ff140a8a1..e74c26d6d3 100644 --- a/crates/neomacs-layout-engine/src/font/resolver.rs +++ b/crates/neomacs-layout-engine/src/font/resolver.rs @@ -678,7 +678,9 @@ impl FontResolver { let request = SelectionRequest { weight: policy.weight, slant: policy.slant, - width: policy.explicit_width, + // GNU font_select_entity prefers the face's width when the + // fontset spec leaves it unset, as for weight and slant. + width: Some(policy.width), spacing: None, prefer_monospace, queried_family: family.as_deref(), @@ -728,7 +730,6 @@ struct CapturedCharacterPolicy { weight: u16, slant: FontSlant, width: FontWidth, - explicit_width: Option, size: FontSelectionSize, } @@ -759,7 +760,6 @@ impl CapturedCharacterPolicy { .unwrap_or(weight), slant: spec.slant.unwrap_or(slant), width: spec.width.unwrap_or(width), - explicit_width: spec.width, size, } } diff --git a/crates/neomacs-layout-engine/src/font/resolver/tests/mod.rs b/crates/neomacs-layout-engine/src/font/resolver/tests/mod.rs index 8be27087ef..ffb7a12bab 100644 --- a/crates/neomacs-layout-engine/src/font/resolver/tests/mod.rs +++ b/crates/neomacs-layout-engine/src/font/resolver/tests/mod.rs @@ -551,6 +551,107 @@ fn equal_score_entities_keep_their_own_discovery_order() { ); } +#[test] +fn character_fallback_prefers_the_requested_face_width() { + // A collection such as Iosevka lists its Extended face (width 125) before + // the Regular one. GNU font_select_entity takes the preferred width from + // the face when the fontset spec leaves it unset, so the face width must + // decide, not discovery order. + let collection_face = |index: u32, width: FontWidth| { + let mut face = candidate("Fixture Mono", 400, FontSlant::Normal, 100); + face.matched.metadata.width = Some(width); + replace_file_identity( + &mut face, + ResolvedFontIdentity::from_file("/fixture/FixtureMono.ttc", index, None), + ); + face + }; + let resolver = FontResolver::new(Box::new(CandidateBackend { + candidates: vec![ + collection_face(3, FontWidth::Expanded), + collection_face(0, FontWidth::Normal), + ], + })); + for (requested, expected_index) in [(FontWidth::Normal, 0), (FontWidth::Expanded, 3)] { + let selected = resolver + .resolve_for_char( + "Fixture Mono", + 'α', + 400, + FontSlant::Normal, + requested, + selection_size(), + ) + .expect("covering collection face"); + assert_eq!( + (selected.metadata.width, selected.identity.file_face_index()), + (Some(requested), expected_index), + "requested {requested:?}" + ); + } +} + +#[test] +fn explicit_fontset_width_beats_the_face_width_on_live_and_frozen_paths() { + // The face width only fills an unset fontset width. An explicit + // `:width' in the fontset spec still wins, on the live path and on the + // frozen worker path, both on a fresh lookup and on a cache hit. The + // Normal face is discovered first, so only scoring can choose Expanded. + let collection_face = |index: u32, width: FontWidth| { + let mut face = candidate("Fixture Mono", 400, FontSlant::Normal, 100); + face.matched.metadata.width = Some(width); + replace_file_identity( + &mut face, + ResolvedFontIdentity::from_file("/fixture/FixtureMono.ttc", index, None), + ); + face + }; + let candidates = vec![ + collection_face(0, FontWidth::Normal), + collection_face(3, FontWidth::Expanded), + ]; + let mut eval = neovm_core::emacs_core::Context::new(); + eval.eval_str( + "(let ((font-encoding-alist '((\".*\" unicode)))) \ + (set-fontset-font t ?α (font-spec :family \"Fixture Mono\" :width 'expanded \ + :registry \"iso10646-1\")))", + ) + .unwrap(); + let policies = + FrozenCharacterPolicies::capture(&[("Base Mono", 'α', 400, false, selection_size())], 4096) + .unwrap(); + let selected_face = |resolver: &FontResolver| { + resolver + .resolve_for_char( + "Base Mono", + 'α', + 400, + FontSlant::Normal, + FontWidth::Normal, + selection_size(), + ) + .map(|selected| (selected.metadata.width, selected.identity.file_face_index())) + }; + let expanded = Some((Some(FontWidth::Expanded), 3)); + + let live = FontResolver::new(Box::new(CandidateBackend { + candidates: candidates.clone(), + })); + assert_eq!(selected_face(&live), expanded, "live lookup"); + assert_eq!(selected_face(&live), expanded, "live cache hit"); + drop(eval); + + std::thread::spawn(move || { + let mut frozen = FontResolver::new(Box::new(CandidateBackend { candidates })); + frozen.install_worker_policy(Arc::new(policies)); + assert_eq!(selected_face(&frozen), expanded, "frozen lookup"); + assert_eq!(selected_face(&frozen), expanded, "frozen cache hit"); + assert!(!frozen.worker_policy_missing()); + }) + .join() + .unwrap(); +} + struct MetricBackend { candidates: Vec, probes: Arc, diff --git a/crates/neomacs-layout-engine/src/font/resolver/worker_policy.rs b/crates/neomacs-layout-engine/src/font/resolver/worker_policy.rs index c82bfc5fab..70e2775e61 100644 --- a/crates/neomacs-layout-engine/src/font/resolver/worker_policy.rs +++ b/crates/neomacs-layout-engine/src/font/resolver/worker_policy.rs @@ -152,7 +152,6 @@ impl CapturedCharacterPolicy { .unwrap_or(weight), slant: spec.slant.unwrap_or(slant), width: spec.width.unwrap_or(FontWidth::Normal), - explicit_width: spec.width, size, }) }