Paragraph - font-fallback
feature id | status | description |
---|---|---|
font-fallback | implemented, level-1 | support font fallback in both implicit and explicit mode |
Font fallback is a mechanism used to ensure that text is displayed correctly even when the primary font does not contain glyphs for certain characters. When rendering text, the system first attempts to use the specified primary font. If the font lacks glyphs for some characters, the fallback mechanism searches through a list of alternative fonts to find one that supports those missing glyphs. This process can be implicit, where the system automatically selects fallback fonts based on language and script, or explicit, where specific fallback fonts are provided. The goal is to provide seamless text rendering without visual gaps or missing characters, maintaining the intended appearance and readability across diverse languages and symbols.
Implementation - Level 1
Level 1 exposes a dedicated option for fallback order and blindly passes the specified fonts to Skia. Skia handles the fallback internally, which works well with wide coverage fonts such as Noto Sans CJK. This is the currently implemented level.
Current Implementation
Default Fonts:
- Editor: Inter (supports Latin, Greek, and Cyrillic scripts)
- WASM Bundle: Geist and Geist Mono (embedded to reduce bundle size)
- Note: Geist does not support Greek script
- CJK Fallback: Noto Sans KR, Noto Sans JP, and Noto Sans SC/TC/HK
Implementation Details: The current fallback implementation is a "soft fallback" that relies on Skia's Paragraph engine. It does not perform precise character-by-character font support verification, which means:
- Fallback works well for most common cases
- CJK fonts may have inconsistent fallback behavior across text runs
- Some characters may fall back to different fonts within the same text, leading to visual inconsistencies
Limitations:
- No ICU + Harfbuzz integration for precise Unicode range checking
- CMAP table references are not verified character-by-character
- CJK font fallback can be inconsistent when mixing scripts (e.g., Hangul + Kanji/Hanzi)
- Noto Sans KR has wider coverage than other CJK fonts, which can cause unexpected fallback behavior
Goal
Implicit fallback - operates like a browser, automatically selecting fallback fonts when the primary font lacks glyphs. If the fallback fonts are consistent across platforms, this approach provides consistent rendering output.
Problem: Since we cannot determine which fonts are actually needed for a given text, the system must load all reasonable fallback fonts upfront. This leads to unnecessary memory and storage consumption, as many fonts may never be used.
/// Font fallback manager for Level 1 implementation
/// Handles basic font loading and fallback configuration
impl Interface {
/// Load a font from the given source (file path, bytes, etc.)
/// Returns true if the font was successfully loaded and registered
pub fn load_font(&mut self, source: FontSource) -> bool;
/// List all available font names that can be used for fallback
/// Returns a collection of font family names
pub fn list_available_fonts(&self) -> Vec<String>;
/// Set the default fallback fonts by their family names
/// These fonts will be used in order when primary font lacks glyphs
pub fn set_user_fallback_fonts(&mut self, font_names: Vec<String>);
/// Get the current default fallback fonts by their family names
/// Returns the ordered list of fallback fonts
pub fn get_user_fallback_fonts(&self) -> Vec<String>;
}
Implementation - Level 2
Level 2 introduces a CSS-inspired unicode-range
style fallback control, allowing clients to specify fallback fonts with fine-grained control over which Unicode ranges they cover. This approach provides more precise fallback behavior than Level 1 by enabling selective font usage based on character ranges, improving rendering consistency and reducing unnecessary font loading.
Goal
Unicode-range style fallback - exposes APIs for clients to specify fallback fonts along with the Unicode ranges they cover. This allows the engine to apply fallback fonts only to characters within specified ranges, improving control and efficiency.
Technical Requirements:
- Support for specifying Unicode ranges per fallback font
- Improved fallback resolution based on character code points
- Still relies on Skia's Paragraph engine for rendering
/// Font fallback manager for Level 2 implementation
/// Provides Unicode-range based fallback control
impl Interface {
// ... existing Level 1 methods ...
/// Set fallback fonts with associated Unicode ranges
/// Each font is mapped to one or more Unicode ranges it covers
pub fn set_fallback_fonts_with_unicode_ranges(&mut self, font_ranges: Vec<(String, Vec<UnicodeRange>)>);
/// Get the current fallback fonts along with their Unicode ranges
pub fn get_fallback_fonts_with_unicode_ranges(&self) -> Vec<(String, Vec<UnicodeRange>)>;
}
Implementation - Level 3
Level 3 involves the engine detecting missing glyphs and exposing APIs to the editor/frontend to identify which characters require additional font loading. It resolves all fallback fonts explicitly before passing the text to Skia. This approach implies persistence, as the fallback information needs to be stored in the design document. The benefit is that designs render identically across platforms as long as the fallback set remains stable.
Goal
Explicit fallback - exposes full APIs for testing and resolving text/font relationships in an explicit manner. This approach aims to provide the capability for clients to specify the exact fonts for missing characters, ensuring a persistent storage model that maintains design consistency across different environments.
Technical Requirements:
- ICU + Harfbuzz integration for precise Unicode range checking
- Character-by-character font support verification
- Abandoning Skia's Paragraph engine (major architectural change)
/// Font fallback manager for Level 3 implementation
/// Provides advanced glyph analysis and explicit font resolution
impl Interface {
// ... existing Level 1 methods ...
/// Analyze which characters in the given text cannot be rendered
/// with the current font set. Returns analysis result for editor use.
/// Editor will use this to find/load/fetch fonts from server and register them.
pub fn analyze_character_font_availability(&self, text: &str, primary_font: &str) -> FontAvailabilityAnalysis;
/// Get characters that will be resolved by the given analysis result
/// This helps the editor understand which characters need font resolution
/// before rendering to ensure all runs explicitly have a font assigned.
pub fn get_characters_for_resolution(&self, analysis: &FontAvailabilityAnalysis) -> Vec<char>;
}
Implementation - Status
Level 1: ✅ Implemented - Currently in production with soft fallback support
Level 2: ❌ Not yet implemented - Planned Unicode-range style fallback control
Level 3: ❌ Not planned - Would require abandoning Skia's Paragraph engine
Current Limitations
The soft fallback approach (Level 1) works well for most use cases but has some limitations:
- CJK Font Inconsistency: When mixing Hangul with Kanji/Hanzi, fallback may be inconsistent across text runs
- Font Coverage Assumptions: Relies on font CMAP tables without precise character verification
- Bundle Size Trade-offs: WASM bundle uses Geist instead of Inter to reduce size, but loses Greek script support
Level 2 aims to address some of these issues by allowing more precise fallback control via Unicode ranges, but it is not yet implemented.
Level 3 would provide full explicit fallback control with glyph analysis but requires major architectural changes.
Future Considerations
Implementing precise, explicit font fallback would require:
- ICU + Harfbuzz integration
- Character-by-character font support verification
- Major architectural changes to move away from Skia's Paragraph engine
- This will be revisited in the future when the benefits outweigh the implementation complexity
Level 2 fallback control based on Unicode ranges is planned to improve fallback precision and efficiency without abandoning Skia.