diff --git a/crates/rustmotion-components/src/avatar_group.rs b/crates/rustmotion-components/src/avatar_group.rs index 4f5bac1..aa8d02a 100644 --- a/crates/rustmotion-components/src/avatar_group.rs +++ b/crates/rustmotion-components/src/avatar_group.rs @@ -7,8 +7,8 @@ use skia_safe::{Canvas, Paint, PaintStyle, RRect, Rect}; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - asset_cache, draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, - paint_from_hex, + asset_cache, draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, + paint_from_hex, typeface_with_fallback, }; use rustmotion_core::error::RustmotionError; use rustmotion_core::schema::TimelineStep; @@ -164,13 +164,10 @@ impl AvatarGroup { // Text let text = format!("+{}", overflow); let font_size = s * 0.35; - let fm = font_mgr(); let font_style = skia_safe::FontStyle::bold(); - let typeface = fm - .match_family_style("Inter", font_style) - .or_else(|| fm.match_family_style("Helvetica", font_style)) - .or_else(|| fm.match_family_style("Arial", font_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, font_style).unwrap()); + let Ok(typeface) = typeface_with_fallback("Inter", font_style) else { + return Ok(()); + }; let font = skia_safe::Font::from_typeface(typeface, font_size); let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, font_size)); diff --git a/crates/rustmotion-components/src/badge.rs b/crates/rustmotion-components/src/badge.rs index da60b8e..d0f4598 100644 --- a/crates/rustmotion-components/src/badge.rs +++ b/crates/rustmotion-components/src/badge.rs @@ -6,10 +6,9 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - asset_cache, draw_text_with_fallback, emoji_typeface, fetch_icon_svg, font_mgr, - measure_text_with_fallback, paint_from_hex, + asset_cache, draw_text_with_fallback, emoji_typeface, fetch_icon_svg, + measure_text_with_fallback, paint_from_hex, typeface_with_fallback, }; -use rustmotion_core::error::RustmotionError; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -95,20 +94,14 @@ impl Badge { (h_pad * ratio, v_pad * ratio, icon_size * ratio) } - fn make_font(&self) -> skia_safe::Font { - let fm = font_mgr(); + fn make_font(&self) -> Option { let font_style = skia_safe::FontStyle::normal(); let family = self.style.font_family.as_deref().unwrap_or("Inter"); - - let typeface = fm - .match_family_style(family, font_style) - .or_else(|| fm.match_family_style("Helvetica", font_style)) - .or_else(|| fm.match_family_style("Arial", font_style)) - .or_else(|| fm.match_family_style("sans-serif", font_style)) - .or_else(|| fm.legacy_make_typeface(None, font_style)) - .unwrap_or_else(|| panic!("{}", RustmotionError::FontNotFound.to_string())); - - skia_safe::Font::from_typeface(typeface, self.resolved_font_size()) + let typeface = typeface_with_fallback(family, font_style).ok()?; + Some(skia_safe::Font::from_typeface( + typeface, + self.resolved_font_size(), + )) } } @@ -209,7 +202,9 @@ impl Badge { } else { color }; - let font = self.make_font(); + let Some(font) = self.make_font() else { + return; + }; let font_size = self.resolved_font_size(); let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, font_size)); let mut text_paint = paint_from_hex(text_color); @@ -270,15 +265,10 @@ impl Badge { }; let count_fs = font_size * 0.65; - let fm = font_mgr(); - let count_typeface = fm - .match_family_style("Inter", skia_safe::FontStyle::bold()) - .or_else(|| fm.match_family_style("Helvetica", skia_safe::FontStyle::bold())) - .or_else(|| fm.match_family_style("Arial", skia_safe::FontStyle::bold())) - .unwrap_or_else(|| { - fm.legacy_make_typeface(None, skia_safe::FontStyle::bold()) - .unwrap() - }); + let Ok(count_typeface) = typeface_with_fallback("Inter", skia_safe::FontStyle::bold()) + else { + return; + }; let count_font = skia_safe::Font::from_typeface(count_typeface, count_fs); let count_emoji = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, count_fs)); diff --git a/crates/rustmotion-components/src/callout.rs b/crates/rustmotion-components/src/callout.rs index 64fe726..ab9e880 100644 --- a/crates/rustmotion-components/src/callout.rs +++ b/crates/rustmotion-components/src/callout.rs @@ -6,8 +6,7 @@ use skia_safe::{Canvas, PaintStyle, Path, RRect, Rect}; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; -use rustmotion_core::engine::renderer::{font_mgr, paint_from_hex, wrap_text}; -use rustmotion_core::error::RustmotionError; +use rustmotion_core::engine::renderer::{paint_from_hex, typeface_with_fallback, wrap_text}; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -147,14 +146,9 @@ impl Callout { canvas.draw_path(&arrow, &bg_paint); // Draw text - let fm = font_mgr(); let font_style = skia_safe::FontStyle::normal(); let family = self.style.font_family.as_deref().unwrap_or("Inter"); - let typeface = fm - .match_family_style(family, font_style) - .or_else(|| fm.match_family_style("Helvetica", font_style)) - .or_else(|| fm.match_family_style("Arial", font_style)) - .ok_or(RustmotionError::FontNotFound)?; + let typeface = typeface_with_fallback(family, font_style)?; let font = skia_safe::Font::from_typeface(typeface, font_size); let (_, metrics) = font.metrics(); diff --git a/crates/rustmotion-components/src/caption.rs b/crates/rustmotion-components/src/caption.rs index 611abdd..67c2ee3 100644 --- a/crates/rustmotion-components/src/caption.rs +++ b/crates/rustmotion-components/src/caption.rs @@ -6,7 +6,8 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, paint_from_hex, + draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, paint_from_hex, + typeface_with_fallback, }; use rustmotion_core::schema::{CaptionStyle, CaptionWord, TimelineStep}; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -42,16 +43,9 @@ impl Caption { let color = self.style.color_str_or("#FFFFFF"); let font_family = self.style.font_family_or("Inter"); - let fm = font_mgr(); - let typeface = fm - .match_family_style(font_family, FontStyle::bold()) - .or_else(|| fm.match_family_style("Helvetica", FontStyle::bold())) - .or_else(|| fm.match_family_style("Arial", FontStyle::bold())) - .or_else(|| fm.match_family_style("sans-serif", FontStyle::bold())) - .unwrap_or_else(|| { - fm.legacy_make_typeface(None, FontStyle::bold()) - .expect("No fallback font") - }); + let Ok(typeface) = typeface_with_fallback(font_family, FontStyle::bold()) else { + return; + }; let font = Font::from_typeface(typeface, font_size); let emoji_font = emoji_typeface().map(|tf| Font::from_typeface(tf, font_size)); diff --git a/crates/rustmotion-components/src/chart/axes.rs b/crates/rustmotion-components/src/chart/axes.rs index f80a4ca..deb83cc 100644 --- a/crates/rustmotion-components/src/chart/axes.rs +++ b/crates/rustmotion-components/src/chart/axes.rs @@ -52,7 +52,9 @@ impl Chart { x_labels: &[String], categorical: bool, ) { - let font = self.make_label_font(); + let Some(font) = self.make_label_font() else { + return; + }; let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, self.label_font_size)); let (_, metrics) = font.metrics(); diff --git a/crates/rustmotion-components/src/chart/bar.rs b/crates/rustmotion-components/src/chart/bar.rs index 88a07fb..93fb44f 100644 --- a/crates/rustmotion-components/src/chart/bar.rs +++ b/crates/rustmotion-components/src/chart/bar.rs @@ -78,7 +78,9 @@ impl Chart { let gap = 8.0; let bar_h = (chart_h - gap * (n + 1) as f32) / n as f32; - let font = self.make_label_font(); + let Some(font) = self.make_label_font() else { + return Ok(()); + }; let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, self.label_font_size)); let (_, metrics) = font.metrics(); diff --git a/crates/rustmotion-components/src/chart/funnel.rs b/crates/rustmotion-components/src/chart/funnel.rs index 5540b4c..ccb02bc 100644 --- a/crates/rustmotion-components/src/chart/funnel.rs +++ b/crates/rustmotion-components/src/chart/funnel.rs @@ -42,7 +42,9 @@ impl Chart { let total_gap = gap * (n.saturating_sub(1)) as f32; let seg_h = (h - total_gap) / n as f32; - let font = self.make_label_font(); + let Some(font) = self.make_label_font() else { + return Ok(()); + }; let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, self.label_font_size)); let (_, metrics) = font.metrics(); @@ -120,7 +122,9 @@ impl Chart { let total_gap = gap * (n.saturating_sub(1)) as f32; let seg_w = (w - total_gap) / n as f32; - let font = self.make_label_font(); + let Some(font) = self.make_label_font() else { + return Ok(()); + }; let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, self.label_font_size)); let (_, metrics) = font.metrics(); diff --git a/crates/rustmotion-components/src/chart/mod.rs b/crates/rustmotion-components/src/chart/mod.rs index f77fa6e..f115e98 100644 --- a/crates/rustmotion-components/src/chart/mod.rs +++ b/crates/rustmotion-components/src/chart/mod.rs @@ -6,7 +6,7 @@ use skia_safe::Canvas; use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; -use rustmotion_core::engine::renderer::font_mgr; +use rustmotion_core::engine::renderer::typeface_with_fallback; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -286,16 +286,13 @@ impl Chart { (8.0, 8.0, bottom + 8.0, left + 8.0) } - pub(super) fn make_label_font(&self) -> skia_safe::Font { - let fm = font_mgr(); + pub(super) fn make_label_font(&self) -> Option { let font_style = skia_safe::FontStyle::normal(); - let typeface = fm - .match_family_style("Inter", font_style) - .or_else(|| fm.match_family_style("Helvetica", font_style)) - .or_else(|| fm.match_family_style("Arial", font_style)) - .or_else(|| fm.match_family_style("sans-serif", font_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, font_style).unwrap()); - skia_safe::Font::from_typeface(typeface, self.label_font_size) + let typeface = typeface_with_fallback("Inter", font_style).ok()?; + Some(skia_safe::Font::from_typeface( + typeface, + self.label_font_size, + )) } } diff --git a/crates/rustmotion-components/src/chart/radar.rs b/crates/rustmotion-components/src/chart/radar.rs index d78d5f6..1c9607f 100644 --- a/crates/rustmotion-components/src/chart/radar.rs +++ b/crates/rustmotion-components/src/chart/radar.rs @@ -66,7 +66,9 @@ impl Chart { } // Draw axis labels - let font = self.make_label_font(); + let Some(font) = self.make_label_font() else { + return Ok(()); + }; let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, self.label_font_size)); let (_, metrics) = font.metrics(); diff --git a/crates/rustmotion-components/src/codeblock/chrome.rs b/crates/rustmotion-components/src/codeblock/chrome.rs index 3f23b04..204f765 100644 --- a/crates/rustmotion-components/src/codeblock/chrome.rs +++ b/crates/rustmotion-components/src/codeblock/chrome.rs @@ -2,7 +2,8 @@ use skia_safe::{Canvas, Font, FontStyle, Rect}; use super::Codeblock; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, paint_from_hex, + draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, paint_from_hex, + typeface_with_fallback, }; pub(super) fn draw_chrome( @@ -39,15 +40,9 @@ pub(super) fn draw_chrome( } if let Some(ref title) = chrome.title { - let fm = font_mgr(); - let typeface = fm - .match_family_style("Inter", FontStyle::normal()) - .or_else(|| fm.match_family_style("Helvetica", FontStyle::normal())) - .or_else(|| fm.match_family_style("Arial", FontStyle::normal())) - .unwrap_or_else(|| { - fm.match_family_style("sans-serif", FontStyle::normal()) - .unwrap() - }); + let Ok(typeface) = typeface_with_fallback("Inter", FontStyle::normal()) else { + return; + }; let title_font = Font::from_typeface(typeface, 13.0); let emoji_font = emoji_typeface().map(|tf| Font::from_typeface(tf, 13.0)); let title_width = measure_text_with_fallback(title, &title_font, &emoji_font, 0.0); diff --git a/crates/rustmotion-components/src/codeblock/highlight.rs b/crates/rustmotion-components/src/codeblock/highlight.rs index e6a9a97..0432159 100644 --- a/crates/rustmotion-components/src/codeblock/highlight.rs +++ b/crates/rustmotion-components/src/codeblock/highlight.rs @@ -526,7 +526,7 @@ pub(super) fn highlight_code(code: &str, language: &str, theme: &Theme) -> Vec Font { +pub(super) fn resolve_monospace_font(family: &str, size: f32, weight: FontWeight) -> Option { let font_mgr = rustmotion_core::engine::renderer::font_mgr(); let w: i32 = match weight { FontWeight::Normal => 400, @@ -551,14 +551,8 @@ pub(super) fn resolve_monospace_font(family: &str, size: f32, weight: FontWeight .iter() .filter_map(|name| font_mgr.match_family_style(name, style)) .next() - .unwrap_or_else(|| { - if font_mgr.count_families() > 0 { - font_mgr - .match_family_style(font_mgr.family_name(0), style) - .unwrap() - } else { - panic!("No fonts available on this system"); - } - }); - Font::from_typeface(typeface, size) + .or_else(|| { + rustmotion_core::engine::renderer::typeface_with_fallback(family, style).ok() + })?; + Some(Font::from_typeface(typeface, size)) } diff --git a/crates/rustmotion-components/src/codeblock/render.rs b/crates/rustmotion-components/src/codeblock/render.rs index 0a47c88..6c770ba 100644 --- a/crates/rustmotion-components/src/codeblock/render.rs +++ b/crates/rustmotion-components/src/codeblock/render.rs @@ -31,7 +31,9 @@ pub(super) fn render_codeblock( _ => FontWeight::Normal, }; let actual_line_height = layer.style.line_height_for(font_size); - let font = resolve_monospace_font(font_family, font_size, font_weight); + let Some(font) = resolve_monospace_font(font_family, font_size, font_weight) else { + return; + }; let padding = { let (t, r, b, l) = layer.style.padding_px(); if t == 0.0 && r == 0.0 && b == 0.0 && l == 0.0 { diff --git a/crates/rustmotion-components/src/comparison.rs b/crates/rustmotion-components/src/comparison.rs index ec767ae..3b45e2a 100644 --- a/crates/rustmotion-components/src/comparison.rs +++ b/crates/rustmotion-components/src/comparison.rs @@ -6,8 +6,8 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, paint_from_hex, - parse_hex_color, + draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, paint_from_hex, + parse_hex_color, typeface_with_fallback, }; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -127,12 +127,12 @@ impl Comparison { canvas.draw_rect(right_rect, &right_paint); let font_size = (h * 0.08).clamp(16.0, 36.0); - let fm = font_mgr(); let font_style = skia_safe::FontStyle::bold(); - let typeface = fm - .match_family_style("Inter", font_style) - .or_else(|| fm.match_family_style("Helvetica", font_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, font_style).unwrap()); + let Ok(typeface) = typeface_with_fallback("Inter", font_style) else { + // Balance the canvas.save() above before bailing out. + canvas.restore(); + return; + }; let font = skia_safe::Font::from_typeface(typeface, font_size); let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, font_size)); diff --git a/crates/rustmotion-components/src/countdown.rs b/crates/rustmotion-components/src/countdown.rs index d7c8e6a..f7446a8 100644 --- a/crates/rustmotion-components/src/countdown.rs +++ b/crates/rustmotion-components/src/countdown.rs @@ -6,8 +6,8 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, paint_from_hex, - parse_hex_color, + draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, paint_from_hex, + parse_hex_color, typeface_with_fallback, }; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -178,12 +178,10 @@ impl Countdown { let secs = total_secs % 60; let font_size = self.digit_size * 0.6; - let fm = font_mgr(); let font_style = skia_safe::FontStyle::bold(); - let typeface = fm - .match_family_style("Inter", font_style) - .or_else(|| fm.match_family_style("Helvetica", font_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, font_style).unwrap()); + let Ok(typeface) = typeface_with_fallback("Inter", font_style) else { + return; + }; let font = skia_safe::Font::from_typeface(typeface, font_size); let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, font_size)); diff --git a/crates/rustmotion-components/src/counter.rs b/crates/rustmotion-components/src/counter.rs index 8e805a1..2ce60ed 100644 --- a/crates/rustmotion-components/src/counter.rs +++ b/crates/rustmotion-components/src/counter.rs @@ -10,10 +10,9 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, font_mgr, format_counter_value, - measure_text_with_fallback, paint_from_hex, + draw_text_with_fallback, emoji_typeface, format_counter_value, measure_text_with_fallback, + paint_from_hex, typeface_with_fallback, }; -use rustmotion_core::error::RustmotionError; use rustmotion_core::schema::{ EasingType, FontStyleType, FontWeight, Stroke, TextAlign, TextShadow, TimelineStep, }; @@ -104,7 +103,6 @@ impl Counter { &self.suffix, ); - let fm = font_mgr(); let slant = match font_style_type { FontStyleType::Normal => skia_safe::font_style::Slant::Upright, FontStyleType::Italic => skia_safe::font_style::Slant::Italic, @@ -117,12 +115,7 @@ impl Counter { }; let skia_font_style = FontStyle::new(weight, skia_safe::font_style::Width::NORMAL, slant); - let typeface = fm - .match_family_style(font_family, skia_font_style) - .or_else(|| fm.match_family_style("Helvetica", skia_font_style)) - .or_else(|| fm.match_family_style("Arial", skia_font_style)) - .or_else(|| fm.match_family_style("sans-serif", skia_font_style)) - .ok_or(RustmotionError::FontNotFound)?; + let typeface = typeface_with_fallback(font_family, skia_font_style)?; let font = Font::from_typeface(typeface, font_size); let emoji_font = emoji_typeface().map(|tf| Font::from_typeface(tf, font_size)); diff --git a/crates/rustmotion-components/src/dot_map.rs b/crates/rustmotion-components/src/dot_map.rs index 685836e..ae69d1f 100644 --- a/crates/rustmotion-components/src/dot_map.rs +++ b/crates/rustmotion-components/src/dot_map.rs @@ -6,7 +6,8 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, paint_from_hex, + draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, paint_from_hex, + typeface_with_fallback, }; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -182,12 +183,10 @@ impl DotMap { let label_font_size = 12.0_f32; let point_count = self.points.len(); - let fm = font_mgr(); let font_style = skia_safe::FontStyle::normal(); - let typeface = fm - .match_family_style("Inter", font_style) - .or_else(|| fm.match_family_style("Helvetica", font_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, font_style).unwrap()); + let Ok(typeface) = typeface_with_fallback("Inter", font_style) else { + return; + }; let label_font = skia_safe::Font::from_typeface(typeface, label_font_size); let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, label_font_size)); diff --git a/crates/rustmotion-components/src/gauge.rs b/crates/rustmotion-components/src/gauge.rs index 25fc573..c62449d 100644 --- a/crates/rustmotion-components/src/gauge.rs +++ b/crates/rustmotion-components/src/gauge.rs @@ -6,7 +6,8 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, paint_from_hex, + draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, paint_from_hex, + typeface_with_fallback, }; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -136,13 +137,10 @@ impl Gauge { let text = format!("{}", display_val.round() as i64); let font_size = (radius * 0.45).max(16.0); - let fm = font_mgr(); let font_style = skia_safe::FontStyle::bold(); - let typeface = fm - .match_family_style("Inter", font_style) - .or_else(|| fm.match_family_style("Helvetica", font_style)) - .or_else(|| fm.match_family_style("Arial", font_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, font_style).unwrap()); + let Ok(typeface) = typeface_with_fallback("Inter", font_style) else { + return; + }; let font = skia_safe::Font::from_typeface(typeface, font_size); let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, font_size)); @@ -170,13 +168,10 @@ impl Gauge { // Label text if let Some(label) = &self.label { let font_size = (radius * 0.18).max(10.0); - let fm = font_mgr(); let font_style = skia_safe::FontStyle::normal(); - let typeface = fm - .match_family_style("Inter", font_style) - .or_else(|| fm.match_family_style("Helvetica", font_style)) - .or_else(|| fm.match_family_style("Arial", font_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, font_style).unwrap()); + let Ok(typeface) = typeface_with_fallback("Inter", font_style) else { + return; + }; let font = skia_safe::Font::from_typeface(typeface, font_size); let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, font_size)); diff --git a/crates/rustmotion-components/src/gradient_text.rs b/crates/rustmotion-components/src/gradient_text.rs index 5f27cf2..9b4bad8 100644 --- a/crates/rustmotion-components/src/gradient_text.rs +++ b/crates/rustmotion-components/src/gradient_text.rs @@ -9,7 +9,7 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - emoji_typeface, font_mgr, paint_from_hex, parse_hex_color, + emoji_typeface, paint_from_hex, parse_hex_color, typeface_with_fallback, }; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -54,11 +54,10 @@ rustmotion_core::impl_traits!(GradientText { }); impl GradientText { - fn resolve_font(&self) -> (Font, Option) { + fn resolve_font(&self) -> Option<(Font, Option)> { let font_size = self.style.font_size_px_or(48.0); let font_family = self.style.font_family_or("Inter"); - let fm = font_mgr(); let slant = match self.style.font_style { Some(CssFontStyle::Italic) => skia_safe::font_style::Slant::Italic, Some(CssFontStyle::Oblique) => skia_safe::font_style::Slant::Oblique, @@ -73,14 +72,11 @@ impl GradientText { }; let skia_style = FontStyle::new(weight, skia_safe::font_style::Width::NORMAL, slant); - let typeface = fm - .match_family_style(font_family, skia_style) - .or_else(|| fm.match_family_style("Helvetica", skia_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, skia_style).unwrap()); + let typeface = typeface_with_fallback(font_family, skia_style).ok()?; let font = Font::from_typeface(typeface, font_size); let emoji_font = emoji_typeface().map(|tf| Font::from_typeface(tf, font_size)); - (font, emoji_font) + Some((font, emoji_font)) } } @@ -90,7 +86,9 @@ impl GradientText { return; } - let (font, _emoji_font) = self.resolve_font(); + let Some((font, _emoji_font)) = self.resolve_font() else { + return; + }; let _font_size = self.style.font_size_px_or(48.0); // Measure text diff --git a/crates/rustmotion-components/src/intrinsic.rs b/crates/rustmotion-components/src/intrinsic.rs index 082a685..c325e6d 100644 --- a/crates/rustmotion-components/src/intrinsic.rs +++ b/crates/rustmotion-components/src/intrinsic.rs @@ -12,7 +12,7 @@ use rustmotion_core::css::style::{ }; use rustmotion_core::engine::box_tree::{AvailableSpace, IntrinsicMeasure}; use rustmotion_core::engine::renderer::{ - emoji_typeface, font_mgr, format_counter_value, measure_text_with_fallback, + emoji_typeface, format_counter_value, measure_text_with_fallback, typeface_with_fallback, wrap_text_with_fallback, }; @@ -95,7 +95,9 @@ impl IntrinsicMeasure for TextIntrinsic { } }; - let font = self.skia_font(); + let Some(font) = self.skia_font() else { + return (0.0, 0.0); + }; let emoji_font = emoji_typeface().map(|tf| Font::from_typeface(tf, self.font_size)); let wrap_at = if self.wrap { max_width } else { None }; @@ -112,8 +114,7 @@ impl IntrinsicMeasure for TextIntrinsic { } impl TextIntrinsic { - fn skia_font(&self) -> Font { - let fm = font_mgr(); + fn skia_font(&self) -> Option { let slant = if self.italic { skia_safe::font_style::Slant::Italic } else { @@ -122,16 +123,8 @@ impl TextIntrinsic { let weight = skia_safe::font_style::Weight::from(self.weight as i32); let sk_style = SkFontStyle::new(weight, skia_safe::font_style::Width::NORMAL, slant); let family = self.font_family.as_deref().unwrap_or("Inter"); - let typeface = fm - .match_family_style(family, sk_style) - .or_else(|| fm.match_family_style("Helvetica", sk_style)) - .or_else(|| fm.match_family_style("Arial", sk_style)) - .or_else(|| fm.match_family_style("sans-serif", sk_style)) - .unwrap_or_else(|| { - fm.legacy_make_typeface(None, sk_style) - .expect("no fallback font") - }); - Font::from_typeface(typeface, self.font_size) + let typeface = typeface_with_fallback(family, sk_style).ok()?; + Some(Font::from_typeface(typeface, self.font_size)) } } diff --git a/crates/rustmotion-components/src/kbd.rs b/crates/rustmotion-components/src/kbd.rs index 3ddbc58..0512174 100644 --- a/crates/rustmotion-components/src/kbd.rs +++ b/crates/rustmotion-components/src/kbd.rs @@ -6,7 +6,8 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, paint_from_hex, + draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, paint_from_hex, + typeface_with_fallback, }; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -55,18 +56,12 @@ rustmotion_core::impl_traits!(Kbd { }); impl Kbd { - fn make_font(&self) -> skia_safe::Font { + fn make_font(&self) -> Option { let fs = self.style.font_size_px_or(self.font_size); - let fm = font_mgr(); let font_style = skia_safe::FontStyle::normal(); let family = self.style.font_family.as_deref().unwrap_or("SF Mono"); - let typeface = fm - .match_family_style(family, font_style) - .or_else(|| fm.match_family_style("Menlo", font_style)) - .or_else(|| fm.match_family_style("Consolas", font_style)) - .or_else(|| fm.match_family_style("monospace", font_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, font_style).unwrap()); - skia_safe::Font::from_typeface(typeface, fs) + let typeface = typeface_with_fallback(family, font_style).ok()?; + Some(skia_safe::Font::from_typeface(typeface, fs)) } } @@ -106,7 +101,9 @@ impl Kbd { canvas.draw_rrect(face_rrect, &border_paint); // Text centered - let font = self.make_font(); + let Some(font) = self.make_font() else { + return; + }; let fs = self.style.font_size_px_or(self.font_size); let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, fs)); diff --git a/crates/rustmotion-components/src/list.rs b/crates/rustmotion-components/src/list.rs index d487238..12ab7f7 100644 --- a/crates/rustmotion-components/src/list.rs +++ b/crates/rustmotion-components/src/list.rs @@ -7,7 +7,8 @@ use skia_safe::{Canvas, ColorType, ImageInfo, Paint, PaintStyle, Rect}; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - asset_cache, draw_text_with_fallback, emoji_typeface, fetch_icon_svg, font_mgr, paint_from_hex, + asset_cache, draw_text_with_fallback, emoji_typeface, fetch_icon_svg, paint_from_hex, + typeface_with_fallback, }; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -83,15 +84,14 @@ impl List { self.style.font_size_px_or(16.0) } - fn make_font(&self) -> skia_safe::Font { - let fm = font_mgr(); + fn make_font(&self) -> Option { let font_style = skia_safe::FontStyle::normal(); let family = self.style.font_family.as_deref().unwrap_or("Inter"); - let typeface = fm - .match_family_style(family, font_style) - .or_else(|| fm.match_family_style("Helvetica", font_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, font_style).unwrap()); - skia_safe::Font::from_typeface(typeface, self.resolved_font_size()) + let typeface = typeface_with_fallback(family, font_style).ok()?; + Some(skia_safe::Font::from_typeface( + typeface, + self.resolved_font_size(), + )) } fn render_icon_svg( @@ -154,7 +154,9 @@ impl List { impl List { fn paint(&self, canvas: &Canvas) -> Result<()> { - let font = self.make_font(); + let Some(font) = self.make_font() else { + return Ok(()); + }; let font_size = self.resolved_font_size(); let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, font_size)); let text_color = self.style.color_str_or("#FFFFFF"); diff --git a/crates/rustmotion-components/src/notification.rs b/crates/rustmotion-components/src/notification.rs index 0ebdcec..f597d5b 100644 --- a/crates/rustmotion-components/src/notification.rs +++ b/crates/rustmotion-components/src/notification.rs @@ -7,7 +7,8 @@ use skia_safe::{Canvas, ColorType, ImageInfo, Paint, PaintStyle, RRect, Rect}; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - asset_cache, draw_text_with_fallback, emoji_typeface, fetch_icon_svg, font_mgr, paint_from_hex, + asset_cache, draw_text_with_fallback, emoji_typeface, fetch_icon_svg, paint_from_hex, + typeface_with_fallback, }; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -108,19 +109,15 @@ impl Notification { self.style.font_size_px_or(16.0) * 0.85 } - fn make_font(&self, bold: bool, size: f32) -> skia_safe::Font { - let fm = font_mgr(); + fn make_font(&self, bold: bool, size: f32) -> Option { let font_style = if bold { skia_safe::FontStyle::bold() } else { skia_safe::FontStyle::normal() }; let family = self.style.font_family.as_deref().unwrap_or("Inter"); - let typeface = fm - .match_family_style(family, font_style) - .or_else(|| fm.match_family_style("Helvetica", font_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, font_style).unwrap()); - skia_safe::Font::from_typeface(typeface, size) + let typeface = typeface_with_fallback(family, font_style).ok()?; + Some(skia_safe::Font::from_typeface(typeface, size)) } fn compute_opacity(&self, time: f64) -> f32 { @@ -228,6 +225,12 @@ impl Notification { return Ok(()); } + // Resolve the title font before any canvas.save() so an early return + // on font failure keeps save/restore balanced. + let Some(title_font) = self.make_font(true, self.title_font_size()) else { + return Ok(()); + }; + // Stack offset: count how many push_at timestamps have passed, // each one shifts this notification down by one slot with animation. let slot_size = h + self.stack_gap; @@ -294,7 +297,6 @@ impl Notification { } // Title - let title_font = self.make_font(true, self.title_font_size()); let title_fs = self.title_font_size(); let emoji_font_title = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, title_fs)); @@ -319,25 +321,26 @@ impl Notification { // Message if let Some(message) = &self.message { let msg_fs = self.message_font_size(); - let msg_font = self.make_font(false, msg_fs); - let emoji_font_msg = - emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, msg_fs)); - let mut msg_paint = paint_from_hex("#9CA3AF"); - msg_paint.set_anti_alias(true); - - let (_, msg_metrics) = msg_font.metrics(); - let msg_y = title_y + 4.0 + title_fs * 0.3 + (-msg_metrics.ascent); - - draw_text_with_fallback( - canvas, - message, - &msg_font, - &emoji_font_msg, - 0.0, - content_x, - msg_y, - &msg_paint, - ); + if let Some(msg_font) = self.make_font(false, msg_fs) { + let emoji_font_msg = + emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, msg_fs)); + let mut msg_paint = paint_from_hex("#9CA3AF"); + msg_paint.set_anti_alias(true); + + let (_, msg_metrics) = msg_font.metrics(); + let msg_y = title_y + 4.0 + title_fs * 0.3 + (-msg_metrics.ascent); + + draw_text_with_fallback( + canvas, + message, + &msg_font, + &emoji_font_msg, + 0.0, + content_x, + msg_y, + &msg_paint, + ); + } } if opacity < 1.0 { diff --git a/crates/rustmotion-components/src/pill_nav.rs b/crates/rustmotion-components/src/pill_nav.rs index 85c5de6..b0c3dac 100644 --- a/crates/rustmotion-components/src/pill_nav.rs +++ b/crates/rustmotion-components/src/pill_nav.rs @@ -6,7 +6,8 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, paint_from_hex, + draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, paint_from_hex, + typeface_with_fallback, }; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -86,23 +87,22 @@ impl PillNav { self.style.font_size_px_or(14.0) } - fn make_font(&self, bold: bool) -> skia_safe::Font { - let fm = font_mgr(); + fn make_font(&self, bold: bool) -> Option { let font_style = if bold { skia_safe::FontStyle::bold() } else { skia_safe::FontStyle::normal() }; let family = self.style.font_family.as_deref().unwrap_or("Inter"); - let typeface = fm - .match_family_style(family, font_style) - .or_else(|| fm.match_family_style("Helvetica", font_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, font_style).unwrap()); - skia_safe::Font::from_typeface(typeface, self.resolved_font_size()) + let typeface = typeface_with_fallback(family, font_style).ok()?; + Some(skia_safe::Font::from_typeface( + typeface, + self.resolved_font_size(), + )) } - fn compute_tab_layout(&self) -> (f32, Vec, Vec) { - let font = self.make_font(false); + fn compute_tab_layout(&self) -> Option<(f32, Vec, Vec)> { + let font = self.make_font(false)?; let font_size = self.resolved_font_size(); let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, font_size)); let h_pad = font_size * 1.2; @@ -125,7 +125,7 @@ impl PillNav { } let total_w = x + inner_pad; - (total_w, tab_positions, tab_widths) + Some((total_w, tab_positions, tab_widths)) } fn active_at_time(&self, time: f64) -> (u32, Option<(u32, f64)>) { @@ -174,7 +174,9 @@ impl PillNav { bg_paint.set_anti_alias(true); canvas.draw_rrect(outer_rrect, &bg_paint); - let (_total_w, tab_positions, tab_widths) = self.compute_tab_layout(); + let Some((_total_w, tab_positions, tab_widths)) = self.compute_tab_layout() else { + return; + }; let (active, transition_info) = self.active_at_time(time); // Pill indicator @@ -204,7 +206,9 @@ impl PillNav { canvas.draw_rrect(pill_rrect, &pill_paint); // Tab labels - let font = self.make_font(false); + let Some(font) = self.make_font(false) else { + return; + }; let font_size = self.resolved_font_size(); let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, font_size)); let (_, metrics) = font.metrics(); diff --git a/crates/rustmotion-components/src/progress.rs b/crates/rustmotion-components/src/progress.rs index 71bd918..3a0f0d5 100644 --- a/crates/rustmotion-components/src/progress.rs +++ b/crates/rustmotion-components/src/progress.rs @@ -7,8 +7,8 @@ use skia_safe::{Canvas, PaintStyle, RRect, Rect}; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - color4f_from_hex, draw_text_with_fallback, emoji_typeface, font_mgr, - measure_text_with_fallback, paint_from_hex, + color4f_from_hex, draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, + paint_from_hex, typeface_with_fallback, }; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -162,13 +162,10 @@ impl Progress { if self.show_value { let text = format!("{}%", (progress * 100.0).round() as i32); let font_size = (radius * 0.5).max(10.0); - let fm = font_mgr(); let font_style = skia_safe::FontStyle::bold(); - let typeface = fm - .match_family_style("Inter", font_style) - .or_else(|| fm.match_family_style("Helvetica", font_style)) - .or_else(|| fm.match_family_style("Arial", font_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, font_style).unwrap()); + let Ok(typeface) = typeface_with_fallback("Inter", font_style) else { + return Ok(()); + }; let font = skia_safe::Font::from_typeface(typeface, font_size); let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, font_size)); diff --git a/crates/rustmotion-components/src/rich_text.rs b/crates/rustmotion-components/src/rich_text.rs index 917065f..624a79e 100644 --- a/crates/rustmotion-components/src/rich_text.rs +++ b/crates/rustmotion-components/src/rich_text.rs @@ -9,7 +9,8 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, paint_from_hex, + draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, paint_from_hex, + typeface_with_fallback, }; use rustmotion_core::schema::{FontStyleType, FontWeight, TextAlign, TimelineStep}; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -60,8 +61,7 @@ fn make_font( weight: &FontWeight, font_style_type: &FontStyleType, size: f32, -) -> Font { - let fm = font_mgr(); +) -> Option { let slant = match font_style_type { FontStyleType::Normal => skia_safe::font_style::Slant::Upright, FontStyleType::Italic => skia_safe::font_style::Slant::Italic, @@ -73,16 +73,8 @@ fn make_font( FontWeight::Weight(w) => skia_safe::font_style::Weight::from(*w as i32), }; let skia_style = FontStyle::new(weight_val, skia_safe::font_style::Width::NORMAL, slant); - let typeface = fm - .match_family_style(family, skia_style) - .or_else(|| fm.match_family_style("Helvetica", skia_style)) - .or_else(|| fm.match_family_style("Arial", skia_style)) - .or_else(|| fm.match_family_style("sans-serif", skia_style)) - .unwrap_or_else(|| { - fm.legacy_make_typeface(None, skia_style) - .expect("No fallback font") - }); - Font::from_typeface(typeface, size) + let typeface = typeface_with_fallback(family, skia_style).ok()?; + Some(Font::from_typeface(typeface, size)) } /// A prepared span ready for rendering (with resolved font, paint, measurements). @@ -125,27 +117,27 @@ impl RichText { let mut prepared: Vec = self .spans .iter() - .map(|span| { + .filter_map(|span| { let size = span.font_size.unwrap_or(default_size); let family = span.font_family.as_deref().unwrap_or(default_family); let weight = span.font_weight.as_ref().unwrap_or(&default_weight); let fstyle = span.font_style.as_ref().unwrap_or(&default_font_style); let color = span.color.as_deref().unwrap_or(default_color).to_string(); let letter_spacing = span.letter_spacing.unwrap_or(0.0); - let font = make_font(family, weight, fstyle, size); + let font = make_font(family, weight, fstyle, size)?; let emoji_font = emoji_tf .as_ref() .map(|tf| Font::from_typeface(tf.clone(), size)); let width = measure_text_with_fallback(&span.text, &font, &emoji_font, letter_spacing); - PreparedSpan { + Some(PreparedSpan { text: span.text.clone(), font, color, letter_spacing, width, - } + }) }) .collect(); diff --git a/crates/rustmotion-components/src/shape.rs b/crates/rustmotion-components/src/shape.rs index a86b994..17bef1e 100644 --- a/crates/rustmotion-components/src/shape.rs +++ b/crates/rustmotion-components/src/shape.rs @@ -8,9 +8,8 @@ use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ build_shape_path, color4f_from_hex, draw_shape_path, draw_text_with_fallback, emoji_typeface, - font_mgr, measure_text_with_fallback, paint_from_hex, wrap_text_with_fallback, + measure_text_with_fallback, paint_from_hex, typeface_with_fallback, wrap_text_with_fallback, }; -use rustmotion_core::error::RustmotionError; use rustmotion_core::schema::{ Fill, FontWeight, GradientType, ShapeText, ShapeType, Stroke, TextAlign, TimelineStep, }; @@ -165,7 +164,6 @@ fn render_shape_text( let area_w = shape_w - 2.0 * pad; let area_h = shape_h - 2.0 * pad; - let fm = font_mgr(); let font_style = match text.font_weight { FontWeight::Bold => skia_safe::FontStyle::bold(), FontWeight::Normal => skia_safe::FontStyle::normal(), @@ -176,19 +174,7 @@ fn render_shape_text( ), }; - let typeface = fm - .match_family_style(&text.font_family, font_style) - .or_else(|| fm.match_family_style("Helvetica", font_style)) - .or_else(|| fm.match_family_style("Arial", font_style)) - .or_else(|| fm.match_family_style("sans-serif", font_style)) - .or_else(|| { - if fm.count_families() > 0 { - fm.match_family_style(fm.family_name(0), font_style) - } else { - None - } - }) - .ok_or(RustmotionError::FontNotFound)?; + let typeface = typeface_with_fallback(&text.font_family, font_style)?; let font = skia_safe::Font::from_typeface(typeface, text.font_size); let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, text.font_size)); diff --git a/crates/rustmotion-components/src/slider.rs b/crates/rustmotion-components/src/slider.rs index 4cd7d35..6af0b79 100644 --- a/crates/rustmotion-components/src/slider.rs +++ b/crates/rustmotion-components/src/slider.rs @@ -6,7 +6,8 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, paint_from_hex, + draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, paint_from_hex, + typeface_with_fallback, }; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -136,13 +137,10 @@ impl Slider { if self.show_value { let text = format!("{}%", (current * 100.0).round() as i32); let font_size = (self.thumb_size * 0.7).max(12.0); - let fm = font_mgr(); let font_style = skia_safe::FontStyle::normal(); - let typeface = fm - .match_family_style("Inter", font_style) - .or_else(|| fm.match_family_style("Helvetica", font_style)) - .or_else(|| fm.match_family_style("Arial", font_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, font_style).unwrap()); + let Ok(typeface) = typeface_with_fallback("Inter", font_style) else { + return; + }; let font = skia_safe::Font::from_typeface(typeface, font_size); let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, font_size)); diff --git a/crates/rustmotion-components/src/stat.rs b/crates/rustmotion-components/src/stat.rs index cb939d5..71e3ea8 100644 --- a/crates/rustmotion-components/src/stat.rs +++ b/crates/rustmotion-components/src/stat.rs @@ -6,8 +6,8 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - asset_cache, draw_text_with_fallback, emoji_typeface, fetch_icon_svg, font_mgr, - measure_text_with_fallback, paint_from_hex, parse_hex_color, + asset_cache, draw_text_with_fallback, emoji_typeface, fetch_icon_svg, + measure_text_with_fallback, paint_from_hex, parse_hex_color, typeface_with_fallback, }; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -86,7 +86,6 @@ impl Stat { fn paint(&self, canvas: &Canvas, layout_w: f32, layout_h: f32) { let w = layout_w; let h = layout_h; - let fm = font_mgr(); // Background if set if let Some(bg) = self.style.background_color_str() { @@ -105,10 +104,9 @@ impl Stat { // Label (top) if let Some(label) = &self.label { let font_style = skia_safe::FontStyle::normal(); - let typeface = fm - .match_family_style("Inter", font_style) - .or_else(|| fm.match_family_style("Helvetica", font_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, font_style).unwrap()); + let Ok(typeface) = typeface_with_fallback("Inter", font_style) else { + return; + }; let font = skia_safe::Font::from_typeface(typeface, self.label_font_size); let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, self.label_font_size)); @@ -134,10 +132,9 @@ impl Stat { // Value (large) { let font_style = skia_safe::FontStyle::bold(); - let typeface = fm - .match_family_style("Inter", font_style) - .or_else(|| fm.match_family_style("Helvetica", font_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, font_style).unwrap()); + let Ok(typeface) = typeface_with_fallback("Inter", font_style) else { + return; + }; let font = skia_safe::Font::from_typeface(typeface, self.value_font_size); let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, self.value_font_size)); @@ -163,14 +160,9 @@ impl Stat { let val_w = measure_text_with_fallback(&self.value, &font, &emoji_font, 0.0); let trend_fs = self.value_font_size * 0.4; let bold_style = skia_safe::FontStyle::bold(); - let trend_typeface = fm - .match_family_style("Inter", bold_style) - .or_else(|| fm.match_family_style("Helvetica", bold_style)) - .or_else(|| fm.match_family_style("Arial", bold_style)) - .unwrap_or_else(|| { - fm.legacy_make_typeface(None, bold_style) - .expect("no font available") - }); + let Ok(trend_typeface) = typeface_with_fallback("Inter", bold_style) else { + return; + }; let trend_font = skia_safe::Font::from_typeface(trend_typeface, trend_fs); let trend_emoji = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, trend_fs)); diff --git a/crates/rustmotion-components/src/stepper.rs b/crates/rustmotion-components/src/stepper.rs index c7ef762..49248ef 100644 --- a/crates/rustmotion-components/src/stepper.rs +++ b/crates/rustmotion-components/src/stepper.rs @@ -6,8 +6,8 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, paint_from_hex, - parse_hex_color, + draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, paint_from_hex, + parse_hex_color, typeface_with_fallback, }; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -121,18 +121,15 @@ impl Stepper { let current = self.current_step_at(time); let r = self.node_size / 2.0; - let fm = font_mgr(); let font_style = skia_safe::FontStyle::normal(); - let typeface = fm - .match_family_style("Inter", font_style) - .or_else(|| fm.match_family_style("Helvetica", font_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, font_style).unwrap()); + let Ok(typeface) = typeface_with_fallback("Inter", font_style) else { + return; + }; let bold_style = skia_safe::FontStyle::bold(); - let bold_typeface = fm - .match_family_style("Inter", bold_style) - .or_else(|| fm.match_family_style("Helvetica", bold_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, bold_style).unwrap()); + let Ok(bold_typeface) = typeface_with_fallback("Inter", bold_style) else { + return; + }; let number_font_size = r * 0.9; let number_font = skia_safe::Font::from_typeface(&bold_typeface, number_font_size); diff --git a/crates/rustmotion-components/src/switch.rs b/crates/rustmotion-components/src/switch.rs index 31ab1e4..c1769c4 100644 --- a/crates/rustmotion-components/src/switch.rs +++ b/crates/rustmotion-components/src/switch.rs @@ -6,7 +6,7 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, font_mgr, paint_from_hex, + draw_text_with_fallback, emoji_typeface, paint_from_hex, typeface_with_fallback, }; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -130,13 +130,10 @@ impl Switch { // Label if let Some(label) = &self.label { let font_size = (h * 0.5).max(12.0); - let fm = font_mgr(); let font_style = skia_safe::FontStyle::normal(); - let typeface = fm - .match_family_style("Inter", font_style) - .or_else(|| fm.match_family_style("Helvetica", font_style)) - .or_else(|| fm.match_family_style("Arial", font_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, font_style).unwrap()); + let Ok(typeface) = typeface_with_fallback("Inter", font_style) else { + return; + }; let font = skia_safe::Font::from_typeface(typeface, font_size); let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, font_size)); diff --git a/crates/rustmotion-components/src/table.rs b/crates/rustmotion-components/src/table.rs index 0ae74f2..94a43b9 100644 --- a/crates/rustmotion-components/src/table.rs +++ b/crates/rustmotion-components/src/table.rs @@ -6,9 +6,9 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, paint_from_hex, + draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, paint_from_hex, + typeface_with_fallback, }; -use rustmotion_core::error::RustmotionError; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -80,8 +80,7 @@ impl Table { self.font_size() * 2.5 } - fn make_font(&self, bold: bool) -> skia_safe::Font { - let fm = font_mgr(); + fn make_font(&self, bold: bool) -> Option { let font_style = if bold { skia_safe::FontStyle::bold() } else { @@ -89,15 +88,8 @@ impl Table { }; let family = self.style.font_family.as_deref().unwrap_or("Inter"); - - let typeface = fm - .match_family_style(family, font_style) - .or_else(|| fm.match_family_style("Helvetica", font_style)) - .or_else(|| fm.match_family_style("Arial", font_style)) - .or_else(|| fm.legacy_make_typeface(None, font_style)) - .unwrap_or_else(|| panic!("{}", RustmotionError::FontNotFound.to_string())); - - skia_safe::Font::from_typeface(typeface, self.font_size()) + let typeface = typeface_with_fallback(family, font_style).ok()?; + Some(skia_safe::Font::from_typeface(typeface, self.font_size())) } /// Resolve column widths: use explicit widths if provided, else equal distribution. @@ -151,6 +143,15 @@ impl Table { let default_row_colors = vec!["#1F2937".to_string(), "#111827".to_string()]; let row_colors = self.row_colors.as_ref().unwrap_or(&default_row_colors); + // Resolve fonts before the optional clip below so an early return on + // font failure keeps canvas save/restore balanced. + let Some(header_font) = self.make_font(true) else { + return; + }; + let Some(body_font) = self.make_font(false) else { + return; + }; + // Clip to rounded rect if border-radius is set let radius_px = self.style.border_radius_px_or(0.0); let has_radius = radius_px > 0.0; @@ -162,7 +163,6 @@ impl Table { } // Header row - let header_font = self.make_font(true); let font_size = self.font_size(); let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, font_size)); let (_, header_metrics) = header_font.metrics(); @@ -196,7 +196,6 @@ impl Table { } // Data rows - let body_font = self.make_font(false); let (_, body_metrics) = body_font.metrics(); let body_ascent = -body_metrics.ascent; diff --git a/crates/rustmotion-components/src/tag_cloud.rs b/crates/rustmotion-components/src/tag_cloud.rs index ac01765..eedad5a 100644 --- a/crates/rustmotion-components/src/tag_cloud.rs +++ b/crates/rustmotion-components/src/tag_cloud.rs @@ -6,8 +6,8 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, paint_from_hex, - parse_hex_color, + draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, paint_from_hex, + parse_hex_color, typeface_with_fallback, }; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -119,7 +119,6 @@ impl TagCloud { .unwrap_or(std::cmp::Ordering::Equal) }); - let fm = font_mgr(); let tag_count = self.tags.len(); let h_gap = 12.0_f32; let v_gap = 8.0_f32; @@ -138,10 +137,9 @@ impl TagCloud { let tag = &self.tags[idx]; let font_size = self.font_size_for_weight(tag.weight, min_weight, max_weight); let font_style = skia_safe::FontStyle::bold(); - let typeface = fm - .match_family_style("Inter", font_style) - .or_else(|| fm.match_family_style("Helvetica", font_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, font_style).unwrap()); + let Ok(typeface) = typeface_with_fallback("Inter", font_style) else { + continue; + }; let font = skia_safe::Font::from_typeface(typeface, font_size); let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, font_size)); @@ -246,10 +244,9 @@ impl TagCloud { text_paint.set_alpha((tag_alpha * 255.0) as u8); let font_style = skia_safe::FontStyle::bold(); - let typeface = fm - .match_family_style("Inter", font_style) - .or_else(|| fm.match_family_style("Helvetica", font_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, font_style).unwrap()); + let Ok(typeface) = typeface_with_fallback("Inter", font_style) else { + continue; + }; let font = skia_safe::Font::from_typeface(typeface, pt.font_size); let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, pt.font_size)); diff --git a/crates/rustmotion-components/src/terminal.rs b/crates/rustmotion-components/src/terminal.rs index 9f74c2c..689c239 100644 --- a/crates/rustmotion-components/src/terminal.rs +++ b/crates/rustmotion-components/src/terminal.rs @@ -6,9 +6,8 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::{ease, AnimatedProperties}; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, font_mgr, paint_from_hex, + draw_text_with_fallback, emoji_typeface, paint_from_hex, typeface_with_fallback, }; -use rustmotion_core::error::RustmotionError; use rustmotion_core::schema::{CodeblockReveal, RevealMode, TimelineStep}; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -132,22 +131,11 @@ const LINE_HEIGHT: f32 = 22.0; const PADDING: f32 = 16.0; impl Terminal { - fn make_font(&self) -> skia_safe::Font { - let fm = font_mgr(); + fn make_font(&self) -> Option { let font_style = skia_safe::FontStyle::normal(); - - let typeface = fm - .match_family_style("SF Mono", font_style) - .or_else(|| fm.match_family_style("Menlo", font_style)) - .or_else(|| fm.match_family_style("Courier New", font_style)) - .or_else(|| fm.match_family_style("monospace", font_style)) - .or_else(|| fm.match_family_style("Courier", font_style)) - .or_else(|| fm.legacy_make_typeface(None, font_style)) - .unwrap_or_else(|| panic!("{}", RustmotionError::FontNotFound.to_string())); - + let typeface = typeface_with_fallback("SF Mono", font_style).ok()?; let size = self.style.font_size_px_or(FONT_SIZE); - - skia_safe::Font::from_typeface(typeface, size) + Some(skia_safe::Font::from_typeface(typeface, size)) } fn line_height(&self) -> f32 { @@ -238,6 +226,12 @@ impl Terminal { bg_paint.set_anti_alias(true); canvas.draw_rrect(bg_rrect, &bg_paint); + // Resolve the terminal font up front — bail before any canvas.save() + // so save/restore stays balanced if no font is available. + let Some(font) = self.make_font() else { + return; + }; + // Always clip content to the rounded box so scrolled lines fade // cleanly at the edges and never escape the device viewport. canvas.save(); @@ -269,7 +263,6 @@ impl Terminal { // Title if let Some(title) = &self.title { - let font = self.make_font(); let font_size = self.style.font_size_px_or(FONT_SIZE); let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, font_size)); @@ -294,7 +287,6 @@ impl Terminal { let (visible_lines, partial_chars, last_line_opacity) = self.compute_reveal(time); // Lines - let font = self.make_font(); let font_size = self.style.font_size_px_or(FONT_SIZE); let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, font_size)); let (_, metrics) = font.metrics(); diff --git a/crates/rustmotion-components/src/text.rs b/crates/rustmotion-components/src/text.rs index c8dd15f..a3ded3d 100644 --- a/crates/rustmotion-components/src/text.rs +++ b/crates/rustmotion-components/src/text.rs @@ -4,7 +4,6 @@ use serde::{Deserialize, Serialize}; use skia_safe::{Canvas, Font, FontStyle, Paint, PaintStyle, Rect}; use rustmotion_core::engine::animator::ease; -use rustmotion_core::error::RustmotionError; use rustmotion_core::css::style::{ FontStyle as CssFontStyle, FontWeight as CssFontWeight, FontWeightKw, TextAlign as CssTextAlign, @@ -13,8 +12,8 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, paint_from_hex, - wrap_text_with_fallback, + draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, paint_from_hex, + typeface_with_fallback, wrap_text_with_fallback, }; use rustmotion_core::schema::{ CharAnimPreset, CharAnimation, FontStyleType, FontWeight, Stroke, TextAlign, @@ -341,7 +340,6 @@ impl Text { let line_height_val = self.style.line_height_for(font_size); let letter_spacing = self.style.letter_spacing_px(); - let fm = font_mgr(); let slant = match font_style_type { FontStyleType::Normal => skia_safe::font_style::Slant::Upright, FontStyleType::Italic => skia_safe::font_style::Slant::Italic, @@ -354,19 +352,7 @@ impl Text { }; let skia_font_style = FontStyle::new(weight, skia_safe::font_style::Width::NORMAL, slant); - let typeface = fm - .match_family_style(font_family, skia_font_style) - .or_else(|| fm.match_family_style("Helvetica", skia_font_style)) - .or_else(|| fm.match_family_style("Arial", skia_font_style)) - .or_else(|| fm.match_family_style("sans-serif", skia_font_style)) - .or_else(|| { - if fm.count_families() > 0 { - fm.match_family_style(fm.family_name(0), skia_font_style) - } else { - None - } - }) - .ok_or(RustmotionError::FontNotFound)?; + let typeface = typeface_with_fallback(font_family, skia_font_style)?; let font = Font::from_typeface(typeface, font_size); let emoji_font = emoji_typeface().map(|tf| Font::from_typeface(tf, font_size)); diff --git a/crates/rustmotion-components/src/timeline.rs b/crates/rustmotion-components/src/timeline.rs index 99353d6..fed10ff 100644 --- a/crates/rustmotion-components/src/timeline.rs +++ b/crates/rustmotion-components/src/timeline.rs @@ -6,7 +6,8 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, paint_from_hex, + draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, paint_from_hex, + typeface_with_fallback, }; use rustmotion_core::schema::TimelineStep as AnimTimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -123,16 +124,9 @@ impl Timeline { return; } - let fm = font_mgr(); - let typeface = fm - .match_family_style("Inter", FontStyle::normal()) - .or_else(|| fm.match_family_style("Helvetica", FontStyle::normal())) - .or_else(|| fm.match_family_style("Arial", FontStyle::normal())) - .or_else(|| fm.match_family_style("sans-serif", FontStyle::normal())) - .unwrap_or_else(|| { - fm.legacy_make_typeface(None, FontStyle::normal()) - .expect("No fallback font") - }); + let Ok(typeface) = typeface_with_fallback("Inter", FontStyle::normal()) else { + return; + }; let font = Font::from_typeface(&typeface, self.font_size); let icon_font = Font::from_typeface(&typeface, self.node_radius * 0.8); let emoji_font = emoji_typeface().map(|tf| Font::from_typeface(tf, self.node_radius * 0.8)); diff --git a/crates/rustmotion-components/src/tooltip.rs b/crates/rustmotion-components/src/tooltip.rs index 7949fa1..ded73d0 100644 --- a/crates/rustmotion-components/src/tooltip.rs +++ b/crates/rustmotion-components/src/tooltip.rs @@ -6,7 +6,8 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, paint_from_hex, + draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, paint_from_hex, + typeface_with_fallback, }; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -72,17 +73,12 @@ rustmotion_core::impl_traits!(Tooltip { }); impl Tooltip { - fn make_font(&self) -> skia_safe::Font { + fn make_font(&self) -> Option { let fs = self.style.font_size_px_or(self.font_size); - let fm = font_mgr(); let font_style = skia_safe::FontStyle::normal(); let family = self.style.font_family_or("Inter"); - let typeface = fm - .match_family_style(family, font_style) - .or_else(|| fm.match_family_style("Helvetica", font_style)) - .or_else(|| fm.match_family_style("Arial", font_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, font_style).unwrap()); - skia_safe::Font::from_typeface(typeface, fs) + let typeface = typeface_with_fallback(family, font_style).ok()?; + Some(skia_safe::Font::from_typeface(typeface, fs)) } } @@ -166,7 +162,9 @@ impl Tooltip { } // Text centered in body - let font = self.make_font(); + let Some(font) = self.make_font() else { + return; + }; let fs = self.style.font_size_px_or(self.font_size); let emoji_font = emoji_typeface().map(|tf| skia_safe::Font::from_typeface(tf, fs)); diff --git a/crates/rustmotion-components/src/treemap.rs b/crates/rustmotion-components/src/treemap.rs index af457f8..b29195d 100644 --- a/crates/rustmotion-components/src/treemap.rs +++ b/crates/rustmotion-components/src/treemap.rs @@ -6,7 +6,8 @@ use rustmotion_core::css::CssStyle; use rustmotion_core::engine::animator::AnimatedProperties; use rustmotion_core::engine::layout_pass::BoxLayout; use rustmotion_core::engine::renderer::{ - draw_text_with_fallback, emoji_typeface, font_mgr, measure_text_with_fallback, paint_from_hex, + draw_text_with_fallback, emoji_typeface, measure_text_with_fallback, paint_from_hex, + typeface_with_fallback, }; use rustmotion_core::schema::TimelineStep; use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig}; @@ -138,12 +139,10 @@ impl Treemap { let full_rect = Rect::from_xywh(0.0, 0.0, w, h); let rects = layout_treemap(&sorted, full_rect, false); - let fm = font_mgr(); let font_style = skia_safe::FontStyle::normal(); - let typeface = fm - .match_family_style("Inter", font_style) - .or_else(|| fm.match_family_style("Helvetica", font_style)) - .unwrap_or_else(|| fm.legacy_make_typeface(None, font_style).unwrap()); + let Ok(typeface) = typeface_with_fallback("Inter", font_style) else { + return; + }; for (idx, rect) in &rects { let item = &self.data[*idx];