use crate::error::Error;
use crate::visual::Color;
use parley::fontique::GenericFamily;
use parley::style::{
FontFamily, FontFamilyName, FontStyle as ParleyFontStyle, FontWeight, StyleProperty,
};
use parley::{Alignment, AlignmentOptions, FontContext, LayoutContext};
use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::Arc;
use vello_cpu::kurbo::Rect;
pub use crate::ast::TextDecoration;
#[derive(Clone, Debug)]
pub struct TextLayout {
pub lines: Vec<TextLine>,
pub width: f64,
pub height: f64,
}
#[derive(Clone, Debug)]
pub struct Glyph {
pub id: u32,
pub x: f32,
pub y: f32,
pub advance: f32,
}
#[derive(Clone, Debug)]
pub struct TextRun {
pub text: String,
pub text_range: std::ops::Range<usize>,
pub font_data: parley::FontData,
pub font_size: f32,
pub color: Color,
pub advance: f32,
pub glyphs: Vec<Glyph>,
pub is_rtl: bool,
pub baseline_x: f32,
pub baseline_y: f32,
pub url: Option<String>,
pub decoration: TextDecoration,
}
#[derive(Clone, Debug)]
pub struct TextLine {
pub runs: Vec<TextRun>,
pub bounds: Rect,
pub line_height: f32,
}
thread_local! {
pub static FONT_CONTEXT: RefCell<FontContext> = RefCell::new(FontContext::default());
pub static LAYOUT_CONTEXT: RefCell<LayoutContext<Color>> = RefCell::new(LayoutContext::default());
pub static FONT_BYTES: RefCell<HashMap<String, Arc<Vec<u8>>>> = RefCell::new(HashMap::new());
}
pub fn get_font_bytes(family: &str) -> Option<Arc<Vec<u8>>> {
FONT_BYTES.with(|cache| cache.borrow().get(family).cloned())
}
pub fn with_font_context<R, F: FnOnce(&mut FontContext) -> R>(f: F) -> R {
FONT_CONTEXT.with(|cx| f(&mut cx.borrow_mut()))
}
pub fn with_layout_context<R, F: FnOnce(&mut LayoutContext<Color>) -> R>(f: F) -> R {
LAYOUT_CONTEXT.with(|cx| f(&mut cx.borrow_mut()))
}
pub fn with_text_contexts<R, F: FnOnce(&mut FontContext, &mut LayoutContext<Color>) -> R>(
f: F,
) -> R {
FONT_CONTEXT.with(|font_cx| {
LAYOUT_CONTEXT.with(|layout_cx| f(&mut font_cx.borrow_mut(), &mut layout_cx.borrow_mut()))
})
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub enum TextAlign {
#[default]
Left,
Center,
Right,
}
#[derive(Debug, Clone)]
pub struct TextStyle {
pub color: Color,
pub font_family: Vec<String>,
pub font_size: f64,
pub font_weight: String,
pub font_style: String,
pub align: TextAlign,
pub url: Option<String>,
pub decoration: TextDecoration,
}
impl Default for TextStyle {
fn default() -> Self {
Self {
color: Color::BLACK,
font_family: vec!["sans-serif".to_string()],
font_size: 10.5,
font_weight: "normal".to_string(),
font_style: "normal".to_string(),
align: TextAlign::Left,
url: None,
decoration: TextDecoration::None,
}
}
}
#[derive(Debug, Clone)]
pub enum FontSource {
Path(std::path::PathBuf),
Memory(Vec<u8>),
}
pub fn register_font(
source: FontSource,
family_name_override: Option<&str>,
) -> crate::error::Result<()> {
use parley::fontique::Blob;
let (data, arc_bytes) = match source {
FontSource::Path(path) => {
let bytes = std::fs::read(&path)
.map_err(|e| Error::FontLoadError(format!("读取字体文件失败: {e}")))?;
let arc = Arc::new(bytes);
(Blob::new(arc.clone()), arc)
}
FontSource::Memory(bytes) => {
let arc = Arc::new(bytes);
(Blob::new(arc.clone()), arc)
}
};
let override_info = family_name_override.map(|name| parley::fontique::FontInfoOverride {
family_name: Some(name),
..Default::default()
});
crate::text::with_font_context(|font_cx| {
font_cx.collection.register_fonts(data, override_info);
});
if let Some(family) = family_name_override {
FONT_BYTES.with(|cache| {
cache.borrow_mut().insert(family.to_string(), arc_bytes);
});
}
Ok(())
}
struct GlyphRaw {
id: u32,
x: f32,
y: f32,
advance: f32,
}
fn extract_lines_from_parley(
layout: &parley::Layout<Color>,
_full_text: &str,
decoration_map: &[(std::ops::Range<usize>, TextDecoration)],
) -> Vec<TextLine> {
let mut lines = Vec::new();
let mut row_top_rel = 0.0_f32;
let mut full_text_pos = 0_usize;
for line in layout.lines() {
let metrics = line.metrics();
let line_height = metrics.line_height;
let baseline_y = metrics.baseline - row_top_rel;
let mut glyph_data: Vec<(GlyphRaw, usize)> = Vec::new();
let mut run_infos: Vec<(
Color,
parley::FontData,
f32,
parley::layout::Run<'_, Color>,
f32,
)> = Vec::new();
let mut next_run_idx = 0;
for item in line.items() {
if let parley::layout::PositionedLayoutItem::GlyphRun(glyph_run) = item {
let run = glyph_run.run();
let color = glyph_run.style().brush;
let font_data = run.font().clone();
let font_size = run.font_size();
let first_glyph_x = glyph_run
.positioned_glyphs()
.next()
.map(|g| g.x)
.unwrap_or(0.0);
run_infos.push((color, font_data, font_size, *run, first_glyph_x));
for g in glyph_run.positioned_glyphs() {
glyph_data.push((
GlyphRaw {
id: g.id,
x: g.x,
y: g.y,
advance: g.advance,
},
next_run_idx,
));
}
next_run_idx += 1;
}
}
if glyph_data.is_empty() {
row_top_rel += line_height;
continue;
}
let min_x = glyph_data
.iter()
.map(|(g, _)| g.x)
.fold(f32::INFINITY, f32::min);
let max_x = glyph_data
.iter()
.map(|(g, _)| g.x)
.fold(f32::NEG_INFINITY, f32::max);
let width = max_x - min_x;
let mut runs = Vec::new();
let mut run_glyph_ranges: Vec<(usize, usize)> = Vec::new();
let mut current_start = 0;
let mut last_run_idx = glyph_data[0].1;
for (i, (_, run_idx)) in glyph_data.iter().enumerate() {
if *run_idx != last_run_idx {
run_glyph_ranges.push((current_start, i));
current_start = i;
last_run_idx = *run_idx;
}
}
run_glyph_ranges.push((current_start, glyph_data.len()));
for (start_idx, end_idx) in run_glyph_ranges.iter() {
let run_idx = glyph_data[*start_idx].1;
let (color, font_data, font_size, _run, first_glyph_x) = &run_infos[run_idx];
let glyph_count = end_idx - start_idx;
let text_range = full_text_pos..full_text_pos + glyph_count;
full_text_pos = text_range.end;
let relative_glyphs: Vec<Glyph> = glyph_data[*start_idx..*end_idx]
.iter()
.map(|(g, _)| Glyph {
id: g.id,
x: g.x - min_x,
y: g.y - row_top_rel,
advance: g.advance,
})
.collect();
let advance = relative_glyphs.iter().map(|g| g.advance).sum();
let baseline_x = *first_glyph_x - min_x;
runs.push(TextRun {
text: String::new(),
text_range: text_range.clone(),
font_data: font_data.clone(),
font_size: *font_size,
color: *color,
advance,
glyphs: relative_glyphs,
is_rtl: false,
baseline_x,
baseline_y,
url: None,
decoration: lookup_decoration(text_range.start, decoration_map),
});
}
let bounds = Rect::new(
min_x as f64,
row_top_rel as f64,
(min_x + width) as f64,
(row_top_rel + line_height) as f64,
);
lines.push(TextLine {
runs,
bounds,
line_height,
});
row_top_rel += line_height;
}
lines
}
fn lookup_decoration(
pos: usize,
map: &[(std::ops::Range<usize>, TextDecoration)],
) -> TextDecoration {
for (range, dec) in map {
if range.contains(&pos) {
return *dec;
}
}
TextDecoration::None
}
pub fn create_text_layout(
text: &str,
font_config: &TextStyle,
max_width: Option<f64>,
) -> TextLayout {
with_text_contexts(|font_cx, layout_cx| {
create_text_layout_with_contexts(text, font_config, max_width, font_cx, layout_cx)
})
}
pub fn create_text_layout_with_contexts(
text: &str,
style: &TextStyle,
max_width: Option<f64>,
font_cx: &mut FontContext,
layout_cx: &mut LayoutContext<Color>,
) -> TextLayout {
let mut builder = layout_cx.ranged_builder(font_cx, text, 1.0, true);
let font_stack = to_parley_font_family(&style.font_family);
builder.push_default(StyleProperty::FontFamily(font_stack));
builder.push_default(StyleProperty::FontSize(style.font_size as f32));
builder.push_default(StyleProperty::Brush(style.color));
builder.push_default(StyleProperty::FontStyle(to_parley_font_style(
&style.font_style,
)));
builder.push_default(StyleProperty::FontWeight(to_parley_font_weight(
&style.font_weight,
)));
let mut layout = builder.build(text);
layout.break_all_lines(max_width.map(|w| w as f32));
layout.align(Alignment::Start, AlignmentOptions::default());
let width = layout.width() as f64;
let height = layout.height() as f64;
let decoration_map = [(0..text.len(), style.decoration)];
let lines = extract_lines_from_parley(&layout, text, &decoration_map);
TextLayout {
lines,
width,
height,
}
}
pub fn layout_text(
texts: &[(&str, &TextStyle)],
max_width: Option<f64>,
align: TextAlign,
) -> TextLayout {
with_text_contexts(|font_cx, layout_cx| {
layout_text_with_contexts(texts, max_width, align, font_cx, layout_cx)
})
}
pub fn layout_text_with_contexts(
texts: &[(&str, &TextStyle)],
max_width: Option<f64>,
align: TextAlign,
font_cx: &mut FontContext,
layout_cx: &mut LayoutContext<Color>,
) -> TextLayout {
if texts.is_empty() {
return layout_text_with_contexts(
&[("", &TextStyle::default())],
max_width,
align,
font_cx,
layout_cx,
);
}
let mut combined = String::new();
let mut ranges: Vec<(usize, usize)> = Vec::with_capacity(texts.len());
for (text, _) in texts {
let start = combined.len();
combined.push_str(text);
let end = combined.len();
ranges.push((start, end));
}
let mut builder = layout_cx.ranged_builder(font_cx, &combined, 1.0, true);
let first_style = &texts[0].1;
let default_font_stack = to_parley_font_family(&first_style.font_family);
builder.push_default(StyleProperty::FontFamily(default_font_stack));
builder.push_default(StyleProperty::FontSize(first_style.font_size as f32));
builder.push_default(StyleProperty::Brush(first_style.color));
builder.push_default(StyleProperty::FontStyle(to_parley_font_style(
&first_style.font_style,
)));
builder.push_default(StyleProperty::FontWeight(to_parley_font_weight(
&first_style.font_weight,
)));
for (i, (_, style)) in texts.iter().enumerate().skip(1) {
let (start, end) = ranges[i];
if start >= end {
continue;
}
builder.push(
StyleProperty::FontFamily(to_parley_font_family(&style.font_family)),
start..end,
);
builder.push(StyleProperty::FontSize(style.font_size as f32), start..end);
builder.push(StyleProperty::Brush(style.color), start..end);
builder.push(
StyleProperty::FontStyle(to_parley_font_style(&style.font_style)),
start..end,
);
builder.push(
StyleProperty::FontWeight(to_parley_font_weight(&style.font_weight)),
start..end,
);
}
let mut layout = builder.build(&combined);
layout.break_all_lines(max_width.map(|w| w as f32));
let paragraph_align = match align {
TextAlign::Left => Alignment::Start,
TextAlign::Center => Alignment::Center,
TextAlign::Right => Alignment::End,
};
layout.align(paragraph_align, AlignmentOptions::default());
let width = layout.width() as f64;
let height = layout.height() as f64;
let mut decoration_map: Vec<(std::ops::Range<usize>, TextDecoration)> = Vec::new();
for (i, (_, style)) in texts.iter().enumerate() {
let (start, end) = ranges[i];
decoration_map.push((start..end, style.decoration));
}
let lines = extract_lines_from_parley(&layout, &combined, &decoration_map);
TextLayout {
lines,
width,
height,
}
}
fn to_parley_font_family(families: &[String]) -> FontFamily<'static> {
let names: Vec<FontFamilyName<'static>> = families
.iter()
.map(|f| match GenericFamily::parse(f) {
Some(generic) => FontFamilyName::Generic(generic),
None => FontFamilyName::Named(Cow::Owned(f.clone())),
})
.collect();
FontFamily::List(Cow::Owned(names))
}
fn to_parley_font_style(style: &str) -> ParleyFontStyle {
match style.to_lowercase().as_str() {
"italic" => ParleyFontStyle::Italic,
"oblique" => ParleyFontStyle::Oblique(None),
_ => ParleyFontStyle::Normal,
}
}
fn to_parley_font_weight(weight: &str) -> FontWeight {
match weight.to_lowercase().as_str() {
"normal" => FontWeight::NORMAL,
"bold" => FontWeight::BOLD,
"thin" | "100" => FontWeight::THIN,
"extra_light" | "200" => FontWeight::EXTRA_LIGHT,
"light" | "300" => FontWeight::LIGHT,
"semi_light" | "350" => FontWeight::SEMI_LIGHT,
"medium" | "500" => FontWeight::MEDIUM,
"semi_bold" | "600" => FontWeight::SEMI_BOLD,
"extra_bold" | "800" => FontWeight::EXTRA_BOLD,
"black" | "900" => FontWeight::BLACK,
_ => {
if let Ok(val) = weight.parse::<f32>() {
FontWeight::new(val)
} else {
FontWeight::NORMAL
}
}
}
}