authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-24 01:05:53-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-25 20:26:20-07:00
log1a88f66f3e89db9e9b9c016c61f9fdd549b74228
tree8d9f3a0f1b27fa6d3f5e702086df07ea4fe2e513
parentb26c81f7c036b864e287820c6d3b0498b64159cf
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

fix: keep digits in the paragraph font when its family is missing

A paragraph whose family is not installed (Calibri on a Mac) fell back per script, and fontique resolved digits to Apple Color Emoji because digits carry emoji keycap presentations. The page date rendered as "Monday, August 1 0 , 2 0 2 6" with 10 pt emoji-width advances. OneNote draws the date in one face. The family list now ends in sans-serif, so a missing family resolves every character the generic face covers to one face; emoji and other scripts still fall back past it. Assisted-by: claude-opus-5

1 files changed, 31 insertions(+), 4 deletions(-)

crates/canvas/src/layout.rs+31-4
...@@ -1,7 +1,8 @@...@@ -1,7 +1,8 @@
1use onestore::page::text::Paragraph;1use onestore::page::text::Paragraph;
2use parley::{2use parley::{
3 Affinity, Alignment, AlignmentOptions, BoundingBox, FontContext, FontFamily, FontStyle,3 Affinity, Alignment, AlignmentOptions, BoundingBox, FontContext, FontFamily, FontFamilyName,
4 FontWeight, Layout, LayoutContext, OverflowWrap, PositionedLayoutItem, StyleProperty,4 FontStyle, FontWeight, GenericFamily, Layout, LayoutContext, OverflowWrap,
5 PositionedLayoutItem, StyleProperty,
5 editing::{Cursor, Selection},6 editing::{Cursor, Selection},
6};7};
7use skrifa::{FontRef, MetadataProvider, raw::TableProvider, string::StringId};8use skrifa::{FontRef, MetadataProvider, raw::TableProvider, string::StringId};
...@@ -139,8 +140,13 @@ impl TextEngine {...@@ -139,8 +140,13 @@ impl TextEngine {
139 return Err(LayoutError::InvalidFontSize);140 return Err(LayoutError::InvalidFontSize);
140 }141 }
141 let properties = [142 let properties = [
142 StyleProperty::FontFamily(FontFamily::named(143 StyleProperty::FontFamily(FontFamily::List(
143 format.font.as_deref().unwrap_or("Arial"),144 vec![
145 FontFamilyName::named(format.font.as_deref().unwrap_or("Arial")),
146 // A missing family otherwise falls back per script and can put digits in an emoji font.
147 GenericFamily::SansSerif.into(),
148 ]
149 .into(),
144 )),150 )),
145 StyleProperty::FontSize(size),151 StyleProperty::FontSize(size),
146 StyleProperty::FontWeight(if format.bold == Some(true) {152 StyleProperty::FontWeight(if format.bold == Some(true) {
...@@ -522,6 +528,27 @@ mod tests {...@@ -522,6 +528,27 @@ mod tests {
522 );528 );
523 }529 }
524530
531 #[test]
532 fn missing_family_keeps_digits_in_the_letters_face() {
533 let paragraph = Paragraph::new(
534 "Monday, August 10, 2026".into(),
535 Format {
536 font: Some("Snowbound Missing Family".into()),
537 ..Format::default()
538 },
539 );
540 let layout = TextEngine::default().layout(&paragraph, 468.0).unwrap();
541 let fonts = layout
542 .lines()
543 .flat_map(|(line, _)| {
544 line.runs()
545 .map(|run| run.font().font.data.id())
546 .collect::<Vec<_>>()
547 })
548 .collect::<BTreeSet<_>>();
549 assert_eq!(fonts.len(), 1);
550 }
551
525 #[test]552 #[test]
526 fn highlights_share_line_geometry_without_changing_wraps() {553 fn highlights_share_line_geometry_without_changing_wraps() {
527 let paragraph = Paragraph::from_runs([554 let paragraph = Paragraph::from_runs([