use std::borrow::Cow;
use std::cell::RefCell;
use parley::{
Alignment, AlignmentOptions, FontContext, LayoutContext,
editing::PlainEditor,
style::{FontFamily, FontFamilyName, FontWeight as ParleyFontWeight, StyleProperty},
};
use crate::geometry::Color;
use crate::view::paint::{FontWeight, TextAlign, TextStyle};
pub type TextLayout = parley::Layout<Color>;
pub type PlainTextEditor = PlainEditor<Color>;
thread_local! {
pub static FONT_CONTEXT: RefCell<FontContext> = RefCell::new(FontContext::default());
pub static LAYOUT_CONTEXT: RefCell<LayoutContext<Color>> = RefCell::new(LayoutContext::new());
}
pub fn with_text_contexts<R, F: FnOnce(&mut FontContext, &mut LayoutContext<Color>) -> R>(
f: F,
) -> R {
FONT_CONTEXT.with(|fc| LAYOUT_CONTEXT.with(|lc| f(&mut fc.borrow_mut(), &mut lc.borrow_mut())))
}
fn apply_text_style(builder: &mut parley::RangedBuilder<Color>, style: &TextStyle) {
builder.push_default(StyleProperty::FontSize(style.font_size as f32));
builder.push_default(StyleProperty::Brush(style.color));
builder.push_default(StyleProperty::FontFamily(FontFamily::named(
&style.font_family,
)));
let pw = match &style.font_weight {
FontWeight::Normal => ParleyFontWeight::NORMAL,
FontWeight::Medium => ParleyFontWeight::MEDIUM,
FontWeight::Bold => ParleyFontWeight::BOLD,
FontWeight::Weight(w) => ParleyFontWeight::new(*w as f32),
};
builder.push_default(StyleProperty::FontWeight(pw));
}
fn map_text_align(a: TextAlign) -> Alignment {
match a {
TextAlign::Start => Alignment::Start,
TextAlign::Center => Alignment::Center,
TextAlign::End => Alignment::End,
TextAlign::Justify => Alignment::Justify,
}
}
pub fn create_text_layout(text: &str, style: &TextStyle) -> TextLayout {
with_text_contexts(|fc, lc| {
let mut builder = lc.ranged_builder(fc, text, 1.0, true);
apply_text_style(&mut builder, style);
let mut layout = builder.build(text);
layout.break_all_lines(style.max_width.map(|w| w as f32));
layout.align(
map_text_align(style.text_align),
AlignmentOptions::default(),
);
layout
})
}
pub struct TextEngine;
impl TextEngine {
pub fn measure_text(text: &str, style: &TextStyle) -> (f64, f64) {
let key = measure_cache_key(text, style);
if let Some(hit) = MEASURE_CACHE.with(|c| c.borrow().get(&key).copied()) {
return hit;
}
let layout = create_text_layout(text, style);
let result = (layout.width() as f64, layout.height() as f64);
MEASURE_CACHE.with(|c| {
let mut c = c.borrow_mut();
if c.len() >= 4096 {
c.clear();
}
c.insert(key, result);
});
result
}
}
type MeasureKey = (String, String, u64, u16, bool, u64, u64);
thread_local! {
static MEASURE_CACHE: RefCell<std::collections::HashMap<MeasureKey, (f64, f64)>> =
RefCell::new(std::collections::HashMap::new());
}
fn measure_cache_key(text: &str, style: &TextStyle) -> MeasureKey {
let weight = match &style.font_weight {
FontWeight::Normal => 400u16,
FontWeight::Medium => 500,
FontWeight::Bold => 700,
FontWeight::Weight(w) => *w,
};
(
text.to_owned(),
style.font_family.clone(),
style.font_size.to_bits(),
weight,
style.wrap,
style.max_width.unwrap_or(f64::INFINITY).to_bits(),
style.line_height.unwrap_or(f64::NAN).to_bits(),
)
}
pub fn apply_plain_editor_style(editor: &mut PlainTextEditor, style: &TextStyle) {
let styles = editor.edit_styles();
styles.insert(StyleProperty::FontSize(style.font_size as f32));
styles.insert(StyleProperty::Brush(style.color));
styles.insert(StyleProperty::FontFamily(FontFamily::Single(
FontFamilyName::Named(Cow::Owned(style.font_family.clone())),
)));
let pw = match &style.font_weight {
FontWeight::Normal => ParleyFontWeight::NORMAL,
FontWeight::Medium => ParleyFontWeight::MEDIUM,
FontWeight::Bold => ParleyFontWeight::BOLD,
FontWeight::Weight(w) => ParleyFontWeight::new(*w as f32),
};
styles.insert(StyleProperty::FontWeight(pw));
}
pub fn create_plain_editor(style: &TextStyle) -> PlainTextEditor {
let mut editor = PlainTextEditor::new(style.font_size as f32);
apply_plain_editor_style(&mut editor, style);
editor
}
pub fn align_to_utf8_boundary(text: &str, mut idx: usize) -> usize {
let len = text.len();
if idx > len {
idx = len;
}
while idx > 0 && !text.is_char_boundary(idx) {
idx -= 1;
}
idx
}
pub fn editor_cursor_geometry(
editor: &mut PlainTextEditor,
style: &TextStyle,
width: Option<f32>,
cursor_width: f32,
) -> Option<parley::BoundingBox> {
with_text_contexts(|font_cx, layout_cx| {
editor.set_width(width);
editor.set_alignment(map_text_align(style.text_align));
editor.refresh_layout(font_cx, layout_cx);
editor.cursor_geometry(cursor_width)
})
}
pub fn editor_layout_size(
editor: &mut PlainTextEditor,
style: &TextStyle,
width: Option<f32>,
) -> (f32, f32) {
with_text_contexts(|font_cx, layout_cx| {
editor.set_width(width);
editor.set_alignment(map_text_align(style.text_align));
editor.refresh_layout(font_cx, layout_cx);
let layout = editor.layout(font_cx, layout_cx);
(layout.width(), layout.height())
})
}
pub fn register_font_bytes(bytes: Vec<u8>) -> Vec<String> {
FONT_CONTEXT.with(|fc| {
let mut fc = fc.borrow_mut();
let blob = parley::fontique::Blob::from(bytes);
let registered = fc.collection.register_fonts(blob, None);
registered
.into_iter()
.filter_map(|(fid, _)| fc.collection.family_name(fid).map(|s| s.to_string()))
.collect()
})
}
pub fn register_font_file<P: AsRef<std::path::Path>>(path: P) -> Vec<String> {
match std::fs::read(path.as_ref()) {
Ok(bytes) => register_font_bytes(bytes),
Err(e) => {
eprintln!("[lieui] 加载字体失败 {:?}: {e}", path.as_ref());
Vec::new()
}
}
}