use std::cell::RefCell;
use std::sync::Arc;
use parley::style::{FontFamily, StyleProperty};
use parley::{FontContext, LayoutContext, Alignment, AlignmentOptions};
use vello_cpu::color::{AlphaColor, Srgb};
use crate::{ChartError, model};
use crate::visual::{TextAlign, TextBaseline, Color};
pub type TextLayout = parley::Layout<TextColor>;
thread_local! {
pub static FONT_CONTEXT: RefCell<FontContext> = RefCell::new(FontContext::default());
pub static LAYOUT_CONTEXT: RefCell<LayoutContext<TextColor>> = RefCell::new(LayoutContext::default());
}
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<TextColor>) -> 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<TextColor>) -> 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(Debug, Clone, Copy, PartialEq)]
pub struct TextColor(pub AlphaColor<Srgb>);
impl TextColor {
pub const BLACK: Self = Self(AlphaColor::BLACK);
pub const WHITE: Self = Self(AlphaColor::WHITE);
pub const RED: Self = Self(AlphaColor::from_rgb8(255, 0, 0));
pub const GREEN: Self = Self(AlphaColor::from_rgb8(0, 128, 0));
pub const BLUE: Self = Self(AlphaColor::from_rgb8(0, 0, 255));
pub fn from_rgba8(r: u8, g: u8, b: u8, a: u8) -> Self {
Self(AlphaColor::from_rgba8(r, g, b, a))
}
pub fn inner(&self) -> AlphaColor<Srgb> {
self.0
}
}
impl Default for TextColor {
fn default() -> Self {
Self(AlphaColor::BLACK)
}
}
fn color_to_text_color(color: &Color) -> TextColor {
TextColor::from_rgba8(color.r, color.g, color.b, color.a)
}
#[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 = match source {
FontSource::Path(path) => {
let bytes = std::fs::read(&path).map_err(|e| {
ChartError::FontLoadError(format!("读取字体文件失败: {e}"))
})?;
Blob::new(Arc::new(bytes))
}
FontSource::Memory(bytes) => Blob::new(Arc::new(bytes)),
};
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);
});
Ok(())
}
pub fn create_text_layout(
text: &str,
font_config: &model::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: &model::TextStyle,
max_width: Option<f64>,
font_cx: &mut FontContext,
layout_cx: &mut LayoutContext<TextColor>,
) -> TextLayout {
let mut builder = layout_cx.ranged_builder(font_cx, text, 1.0, true);
let font_stack = FontFamily::named(&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(color_to_text_color(&style.color)));
let mut layout = builder.build(text);
layout.break_all_lines(max_width.map(|w| w as f32));
layout.align(Alignment::Start, AlignmentOptions::default());
layout
}
pub fn layout_text(
texts: &[(&str, &model::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, &model::TextStyle)],
max_width: Option<f64>,
align: TextAlign,
font_cx: &mut FontContext,
layout_cx: &mut LayoutContext<TextColor>,
) -> TextLayout {
if texts.is_empty() {
return layout_text_with_contexts(&[("", &model::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 = FontFamily::named(&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(color_to_text_color(&first_style.color)));
for (i, (_, style)) in texts.iter().enumerate().skip(1) {
let (start, end) = ranges[i];
if start >= end {
continue;
}
if style.font_family != first_style.font_family {
builder.push(
StyleProperty::FontFamily(FontFamily::named(&style.font_family)),
start..end,
);
}
if (style.font_size - first_style.font_size).abs() > f64::EPSILON {
builder.push(StyleProperty::FontSize(style.font_size as f32), start..end);
}
if style.color != first_style.color {
builder.push(
StyleProperty::Brush(color_to_text_color(&style.color)),
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());
layout
}
pub fn compute_text_offset(
layout: &TextLayout,
align: TextAlign,
baseline: TextBaseline,
) -> (f64, f64) {
let layout_width = layout.width() as f64;
let layout_height = layout.height() as f64;
let x_offset = match align {
TextAlign::Left => 0.0,
TextAlign::Center => -layout_width / 2.0,
TextAlign::Right => -layout_width,
};
let y_offset = match baseline {
TextBaseline::Top => 0.0,
TextBaseline::Middle => -layout_height / 2.0,
TextBaseline::Bottom => -layout_height,
TextBaseline::Alphabetic => {
let first_line = layout.lines().next();
if let Some(line) = first_line {
let line_metrics = line.metrics();
-line_metrics.ascent as f64
} else {
-layout_height * 0.8
}
}
};
(x_offset, y_offset)
}
pub struct TextEngine;
impl TextEngine {
pub fn new() -> Self {
Self
}
pub fn measure_text(
text: &str,
font_size: f64,
font_family: &str,
color: &Color,
max_width: Option<f64>,
) -> (f64, f64) {
let style = model::TextStyle {
font_size,
font_family: font_family.to_string(),
color: *color,
font_weight: crate::option::FontWeight::Named(crate::option::FontWeightNamed::Normal),
..Default::default()
};
let layout = create_text_layout(text, &style, max_width);
(layout.width() as f64, layout.height() as f64)
}
}
impl Default for TextEngine {
fn default() -> Self {
Self::new()
}
}