use core::num::NonZeroUsize;
use femtovg::TextContext;
use i_slint_common::sharedfontdb::{self, fontdb};
use i_slint_core::graphics::euclid;
use i_slint_core::graphics::FontRequest;
use i_slint_core::items::{TextHorizontalAlignment, TextOverflow, TextVerticalAlignment, TextWrap};
use i_slint_core::lengths::PointLengths;
use i_slint_core::lengths::{LogicalLength, LogicalSize, ScaleFactor, SizeLengths};
use i_slint_core::{SharedString, SharedVector};
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use super::{PhysicalLength, PhysicalPoint, PhysicalSize};
pub const DEFAULT_FONT_SIZE: LogicalLength = LogicalLength::new(12.);
#[derive(Clone, PartialEq, Eq, Hash)]
struct FontCacheKey {
family: SharedString,
weight: fontdb::Weight,
style: fontdb::Style,
stretch: fontdb::Stretch,
}
#[derive(Clone)]
pub struct Font {
fonts: SharedVector<femtovg::FontId>,
pixel_size: PhysicalLength,
text_context: TextContext,
}
impl Font {
pub fn init_paint(
&self,
letter_spacing: PhysicalLength,
mut paint: femtovg::Paint,
) -> femtovg::Paint {
paint.set_font(&self.fonts);
paint.set_font_size(self.pixel_size.get());
paint.set_text_baseline(femtovg::Baseline::Top);
paint.set_letter_spacing(letter_spacing.get());
paint
}
pub fn text_size(
&self,
letter_spacing: PhysicalLength,
text: &str,
max_width: Option<PhysicalLength>,
) -> PhysicalSize {
let paint = self.init_paint(letter_spacing, femtovg::Paint::default());
let font_metrics = self.text_context.measure_font(&paint).unwrap();
let mut lines = 0;
let mut width = 0.;
let mut start = 0;
if let Some(max_width) = max_width {
while start < text.len() {
let max_line_index = text[start..].find('\n').map_or(text.len(), |i| i + 1 + start);
let index = self
.text_context
.break_text(max_width.get(), &text[start..max_line_index], &paint)
.unwrap();
if index == 0 {
break;
}
let index = start + index;
let measure =
self.text_context.measure_text(0., 0., &text[start..index], &paint).unwrap();
start = index;
lines += 1;
width = measure.width().max(width);
}
} else {
for line in text.lines() {
let measure = self.text_context.measure_text(0., 0., line, &paint).unwrap();
lines += 1;
width = measure.width().max(width);
}
}
euclid::size2(width, lines as f32 * font_metrics.height())
}
pub fn height(&self) -> PhysicalLength {
let mut paint = femtovg::Paint::default();
paint.set_font(&self.fonts);
paint.set_font_size(self.pixel_size.get());
PhysicalLength::new(self.text_context.measure_font(&paint).unwrap().height())
}
}
pub(crate) fn text_size(
font_request: &i_slint_core::graphics::FontRequest,
scale_factor: ScaleFactor,
text: &str,
max_width: Option<LogicalLength>,
) -> LogicalSize {
let font =
FONT_CACHE.with(|cache| cache.borrow_mut().font(font_request.clone(), scale_factor, text));
let letter_spacing = font_request.letter_spacing.unwrap_or_default();
font.text_size(letter_spacing * scale_factor, text, max_width.map(|x| x * scale_factor))
/ scale_factor
}
#[derive(Copy, Clone)]
struct LoadedFont {
femtovg_font_id: femtovg::FontId,
fontdb_face_id: fontdb::ID,
}
struct SharedFontData(std::sync::Arc<dyn AsRef<[u8]>>);
impl AsRef<[u8]> for SharedFontData {
fn as_ref(&self) -> &[u8] {
self.0.as_ref().as_ref()
}
}
#[derive(Default)]
struct GlyphCoverage {
supported_scripts: HashMap<unicode_script::Script, bool>,
exact_glyph_coverage: HashMap<char, bool>,
}
enum GlyphCoverageCheckResult {
Incomplete,
Improved,
Complete,
}
pub struct FontCache {
loaded_fonts: HashMap<FontCacheKey, LoadedFont>,
loaded_font_coverage: HashMap<fontdb::ID, GlyphCoverage>,
pub(crate) text_context: TextContext,
available_families: HashSet<SharedString>,
}
impl Default for FontCache {
fn default() -> Self {
let available_families = sharedfontdb::FONT_DB.with(|db| {
db.borrow()
.faces()
.filter_map(|face_info| {
face_info.families.first().map(|(family_name, _)| family_name.as_str().into())
})
.collect()
});
let text_context = TextContext::default();
text_context.resize_shaped_words_cache(NonZeroUsize::new(10_000_000).unwrap());
text_context.resize_shaping_run_cache(NonZeroUsize::new(1_000_000).unwrap());
Self {
loaded_fonts: HashMap::new(),
loaded_font_coverage: HashMap::new(),
text_context,
available_families,
}
}
}
thread_local! {
pub static FONT_CACHE: RefCell<FontCache> = RefCell::new(Default::default())
}
impl FontCache {
fn load_single_font(
&mut self,
family: Option<&SharedString>,
query: fontdb::Query<'_>,
) -> LoadedFont {
let text_context = self.text_context.clone();
let cache_key = FontCacheKey {
family: family.cloned().unwrap_or_default(),
weight: query.weight,
style: query.style,
stretch: query.stretch,
};
if let Some(loaded_font) = self.loaded_fonts.get(&cache_key) {
return *loaded_font;
}
let fontdb_face_id = sharedfontdb::FONT_DB.with_borrow(|db| {
db.query_with_family(query, family.map(|s| s.as_str()))
.or_else(|| {
db.query_with_family(query, None)
})
.expect("there must be a sans-serif font face registered")
});
#[cfg(not(target_arch = "wasm32"))]
let (shared_data, face_index) = unsafe {
sharedfontdb::FONT_DB.with_borrow_mut(|db| {
db.make_mut().make_shared_face_data(fontdb_face_id).expect("unable to mmap font")
})
};
#[cfg(target_arch = "wasm32")]
let (shared_data, face_index) = crate::sharedfontdb::FONT_DB.with_borrow(|db| {
db.face_source(fontdb_face_id)
.map(|(source, face_index)| {
(
match source {
fontdb::Source::Binary(data) => data.clone(),
#[allow(unreachable_patterns)]
_ => unreachable!(),
},
face_index,
)
})
.expect("invalid fontdb face id")
});
let femtovg_font_id = text_context
.add_shared_font_with_index(SharedFontData(shared_data), face_index)
.unwrap();
let new_font = LoadedFont { femtovg_font_id, fontdb_face_id };
self.loaded_fonts.insert(cache_key, new_font);
new_font
}
pub fn font(
&mut self,
font_request: FontRequest,
scale_factor: ScaleFactor,
reference_text: &str,
) -> Font {
let pixel_size = font_request.pixel_size.unwrap_or(DEFAULT_FONT_SIZE) * scale_factor;
let query = font_request.to_fontdb_query();
let primary_font = self.load_single_font(font_request.family.as_ref(), query);
use unicode_script::{Script, UnicodeScript};
let mut scripts_required: HashMap<unicode_script::Script, char> = Default::default();
let mut chars_required: HashSet<char> = Default::default();
for ch in reference_text.chars() {
if ch.is_control() || ch.is_whitespace() {
continue;
}
let script = ch.script();
if script == Script::Common || script == Script::Inherited || script == Script::Unknown
{
chars_required.insert(ch);
} else {
scripts_required.insert(script, ch);
}
}
let mut coverage_result = self.check_and_update_script_coverage(
&mut scripts_required,
&mut chars_required,
primary_font.fontdb_face_id,
);
let fallbacks = if !matches!(coverage_result, GlyphCoverageCheckResult::Complete) {
self.font_fallbacks_for_request(
font_request.family.as_ref(),
pixel_size,
&primary_font,
reference_text,
)
} else {
Vec::new()
};
let fonts = core::iter::once(primary_font.femtovg_font_id)
.chain(fallbacks.iter().filter_map(|fallback_family| {
if matches!(coverage_result, GlyphCoverageCheckResult::Complete) {
return None;
}
let fallback_font = self.load_single_font(Some(fallback_family), query);
coverage_result = self.check_and_update_script_coverage(
&mut scripts_required,
&mut chars_required,
fallback_font.fontdb_face_id,
);
if matches!(
coverage_result,
GlyphCoverageCheckResult::Improved | GlyphCoverageCheckResult::Complete
) {
Some(fallback_font.femtovg_font_id)
} else {
None
}
}))
.collect::<SharedVector<_>>();
Font { fonts, text_context: self.text_context.clone(), pixel_size }
}
#[cfg(target_os = "macos")]
fn font_fallbacks_for_request(
&self,
_family: Option<&SharedString>,
_pixel_size: PhysicalLength,
_primary_font: &LoadedFont,
_reference_text: &str,
) -> Vec<SharedString> {
let requested_font = match core_text::font::new_from_name(
&_family.as_ref().map_or_else(|| "", |s| s.as_str()),
_pixel_size.get() as f64,
) {
Ok(f) => f,
Err(_) => return vec![],
};
core_text::font::cascade_list_for_languages(
&requested_font,
&core_foundation::array::CFArray::from_CFTypes(&[]),
)
.iter()
.map(|fallback_descriptor| SharedString::from(fallback_descriptor.family_name()))
.filter(|family| self.is_known_family(&family))
.collect::<Vec<_>>()
}
#[cfg(target_os = "windows")]
fn font_fallbacks_for_request(
&self,
_family: Option<&SharedString>,
_pixel_size: PhysicalLength,
_primary_font: &LoadedFont,
reference_text: &str,
) -> Vec<SharedString> {
let system_font_fallback = match dwrote::FontFallback::get_system_fallback() {
Some(fallback) => fallback,
None => return Vec::new(),
};
let font_collection = dwrote::FontCollection::get_system(false);
let base_family = Some(_family.as_ref().map_or_else(|| "", |s| s.as_str()));
let reference_text_utf16: Vec<u16> = reference_text.encode_utf16().collect();
struct TextAnalysisHack(u32);
impl dwrote::TextAnalysisSourceMethods for TextAnalysisHack {
fn get_locale_name(&self, text_position: u32) -> (std::borrow::Cow<'_, str>, u32) {
("".into(), self.0 - text_position)
}
fn get_paragraph_reading_direction(
&self,
) -> winapi::um::dwrite::DWRITE_READING_DIRECTION {
winapi::um::dwrite::DWRITE_READING_DIRECTION_LEFT_TO_RIGHT
}
}
let text_analysis_source = dwrote::TextAnalysisSource::from_text_and_number_subst(
Box::new(TextAnalysisHack(reference_text_utf16.len() as u32)),
std::borrow::Cow::Borrowed(&reference_text_utf16),
dwrote::NumberSubstitution::new(
winapi::um::dwrite::DWRITE_NUMBER_SUBSTITUTION_METHOD_NONE,
"",
true,
),
);
let mut fallback_fonts = Vec::new();
let mut utf16_pos = 0;
while utf16_pos < reference_text_utf16.len() {
let fallback_result = system_font_fallback.map_characters(
&text_analysis_source,
utf16_pos as u32,
(reference_text_utf16.len() - utf16_pos) as u32,
&font_collection,
base_family,
dwrote::FontWeight::Regular,
dwrote::FontStyle::Normal,
dwrote::FontStretch::Normal,
);
if let Some(fallback_font) = fallback_result.mapped_font {
let family: SharedString = fallback_font.family_name().into();
if self.is_known_family(&family) {
fallback_fonts.push(family)
}
} else {
break;
}
utf16_pos += fallback_result.mapped_length;
}
fallback_fonts
}
#[cfg(not(any(
target_family = "windows",
target_os = "macos",
target_os = "ios",
target_arch = "wasm32",
target_os = "android",
)))]
fn font_fallbacks_for_request(
&self,
_family: Option<&SharedString>,
_pixel_size: PhysicalLength,
_primary_font: &LoadedFont,
_reference_text: &str,
) -> Vec<SharedString> {
sharedfontdb::FONT_DB.with(|db| {
db.borrow()
.fontconfig_fallback_families
.iter()
.filter(|&family_name| self.is_known_family(family_name))
.map(|family_name| family_name.into())
.collect()
})
}
#[cfg(any(target_arch = "wasm32", target_os = "android"))]
fn font_fallbacks_for_request(
&self,
_family: Option<&SharedString>,
_pixel_size: PhysicalLength,
_primary_font: &LoadedFont,
_reference_text: &str,
) -> Vec<SharedString> {
[SharedString::from("DejaVu Sans")]
.iter()
.filter(|family_name| self.is_known_family(&family_name))
.cloned()
.collect()
}
fn is_known_family(&self, family: &str) -> bool {
self.available_families.contains(family)
}
fn check_and_update_script_coverage(
&mut self,
scripts_without_coverage: &mut HashMap<unicode_script::Script, char>,
chars_without_coverage: &mut HashSet<char>,
face_id: fontdb::ID,
) -> GlyphCoverageCheckResult {
let coverage = self.loaded_font_coverage.entry(face_id).or_default();
let mut scripts_that_need_checking = Vec::new();
let mut chars_that_need_checking = Vec::new();
let old_uncovered_scripts_count = scripts_without_coverage.len();
let old_uncovered_chars_count = chars_without_coverage.len();
scripts_without_coverage.retain(|script, sample| {
coverage.supported_scripts.get(script).map_or_else(
|| {
scripts_that_need_checking.push((*script, *sample));
true },
|has_coverage| !has_coverage,
)
});
chars_without_coverage.retain(|ch| {
coverage.exact_glyph_coverage.get(ch).map_or_else(
|| {
chars_that_need_checking.push(*ch);
true },
|has_coverage| !has_coverage,
)
});
if !scripts_that_need_checking.is_empty() || !chars_that_need_checking.is_empty() {
sharedfontdb::FONT_DB.with(|db| {
db.borrow().with_face_data(face_id, |face_data, face_index| {
let face = ttf_parser::Face::parse(face_data, face_index).unwrap();
for (unchecked_script, sample_char) in scripts_that_need_checking {
let glyph_coverage = face.glyph_index(sample_char).is_some();
coverage.supported_scripts.insert(unchecked_script, glyph_coverage);
if glyph_coverage {
scripts_without_coverage.remove(&unchecked_script);
}
}
for unchecked_char in chars_that_need_checking {
let glyph_coverage = face.glyph_index(unchecked_char).is_some();
coverage.exact_glyph_coverage.insert(unchecked_char, glyph_coverage);
if glyph_coverage {
chars_without_coverage.remove(&unchecked_char);
}
}
});
})
}
let remaining_required_script_coverage = scripts_without_coverage.len();
let remaining_required_char_coverage = chars_without_coverage.len();
if scripts_without_coverage.is_empty() && chars_without_coverage.is_empty() {
GlyphCoverageCheckResult::Complete
} else if remaining_required_script_coverage < old_uncovered_scripts_count
|| remaining_required_char_coverage < old_uncovered_chars_count
{
GlyphCoverageCheckResult::Improved
} else {
GlyphCoverageCheckResult::Incomplete
}
}
}
pub(crate) fn layout_text_lines(
string: &str,
font: &Font,
max_size: PhysicalSize,
(horizontal_alignment, vertical_alignment): (TextHorizontalAlignment, TextVerticalAlignment),
wrap: TextWrap,
overflow: TextOverflow,
single_line: bool,
cursor_byte_offset: Option<usize>,
paint: &femtovg::Paint,
mut layout_line: impl FnMut(&str, PhysicalPoint, usize, &femtovg::TextMetrics),
) -> Option<PhysicalPoint> {
let wrap = wrap != TextWrap::NoWrap;
let elide = overflow == TextOverflow::Elide;
let max_width = max_size.width_length();
let max_height = max_size.height_length();
let text_context = FONT_CACHE.with(|cache| cache.borrow().text_context.clone());
let font_metrics = text_context.measure_font(paint).unwrap();
let font_height = PhysicalLength::new(font_metrics.height());
let mut cursor_point: Option<PhysicalPoint> = None;
let text_height = || {
if single_line {
font_height
} else {
let text_height = font
.text_size(
PhysicalLength::new(paint.letter_spacing()),
string,
if wrap { Some(max_width) } else { None },
)
.height_length();
if elide && text_height > max_height {
let max_lines = (max_height.get() / font_height.get()).floor();
font_height * max_lines
} else {
text_height
}
}
};
let mut process_line =
|text_span: &str, y: PhysicalLength, start: usize, line_metrics: &femtovg::TextMetrics| {
let x = match horizontal_alignment {
TextHorizontalAlignment::Left => PhysicalLength::default(),
TextHorizontalAlignment::Center => {
max_width / 2. - max_width.min(PhysicalLength::new(line_metrics.width())) / 2.
}
TextHorizontalAlignment::Right => {
max_width - max_width.min(PhysicalLength::new(line_metrics.width()))
}
};
let line_pos = PhysicalPoint::from_lengths(x, y);
layout_line(text_span, line_pos, start, line_metrics);
if let Some(cursor_byte_offset) = cursor_byte_offset {
let text_span_range = start..(start + text_span.len());
if text_span_range.contains(&cursor_byte_offset)
|| (cursor_byte_offset == text_span_range.end
&& cursor_byte_offset == string.len()
&& !string.ends_with('\n'))
{
let cursor_x = PhysicalLength::new(
line_metrics
.glyphs
.iter()
.find_map(|glyph| {
if glyph.byte_index == (cursor_byte_offset - start) {
Some(glyph.x)
} else {
None
}
})
.unwrap_or_else(|| line_metrics.width()),
);
cursor_point = Some(PhysicalPoint::from_lengths(
line_pos.x_length() + cursor_x,
line_pos.y_length(),
));
}
}
};
let baseline_y = match vertical_alignment {
TextVerticalAlignment::Top => PhysicalLength::default(),
TextVerticalAlignment::Center => max_height / 2. - text_height() / 2.,
TextVerticalAlignment::Bottom => max_height - text_height(),
};
let mut y = baseline_y;
let mut start = 0;
'lines: while start < string.len() && y + font_height <= max_height {
if wrap && (!elide || y + font_height * 2. <= max_height) {
let max_line_index = string[start..].find('\n').map_or(string.len(), |i| i + 1 + start);
let index = text_context
.break_text(max_width.get(), &string[start..max_line_index], paint)
.unwrap();
if index == 0 {
break;
}
let index = start + index;
let line = &string[start..index];
let text_metrics = text_context.measure_text(0., 0., line, paint).unwrap();
process_line(line, y, start, &text_metrics);
y += font_height;
start = index;
} else {
let index = if single_line {
string.len()
} else {
string[start..].find('\n').map_or(string.len(), |i| start + i + 1)
};
let line = &string[start..index];
let text_metrics = text_context.measure_text(0., 0., line, paint).unwrap();
let elide_last_line =
elide && index < string.len() && y + font_height * 2. > max_height;
if text_metrics.width() > max_width.get() || elide_last_line {
let w = max_width
- if elide {
PhysicalLength::new(
text_context.measure_text(0., 0., "…", paint).unwrap().width(),
)
} else {
PhysicalLength::default()
};
let mut current_x = 0.;
for glyph in &text_metrics.glyphs {
current_x += glyph.advance_x;
if current_x >= w.get() {
let txt = &line[..glyph.byte_index];
if elide {
let elided = format!("{}…", txt);
process_line(&elided, y, start, &text_metrics);
} else {
process_line(txt, y, start, &text_metrics);
}
y += font_height;
start = index;
continue 'lines;
}
}
if elide_last_line {
let elided = format!("{}…", line.strip_suffix('\n').unwrap_or(line));
process_line(&elided, y, start, &text_metrics);
y += font_height;
start = index;
continue 'lines;
}
}
process_line(line, y, start, &text_metrics);
y += font_height;
start = index;
}
}
cursor_point.or_else(|| {
cursor_byte_offset.map(|_| {
let x = match horizontal_alignment {
TextHorizontalAlignment::Left => PhysicalLength::default(),
TextHorizontalAlignment::Center => max_size.width_length() / 2.,
TextHorizontalAlignment::Right => max_size.width_length(),
};
PhysicalPoint::from_lengths(x, y)
})
})
}