use std::{cell::RefCell, sync::Arc};
use parley::{
Alignment, AlignmentOptions, FontContext, LayoutContext,
style::{FontFamily, StyleProperty},
};
use crate::{
error::ChartError,
model::TextStyle,
visual::{Color, TextAlign, TextBaseline},
};
pub type TextLayout = parley::Layout<Color>;
thread_local! {
pub static FONT_CONTEXT: RefCell<FontContext> = RefCell::new(FontContext::default());
pub static LAYOUT_CONTEXT: RefCell<LayoutContext<Color>> = 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<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(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!("Failed to read font file: {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: &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 = 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(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, &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 = 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(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(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 = 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()
}
}