liecharts 0.1.0-beta

A Rust charting library with PNG and SVG rendering support
Documentation
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)
    }
}

/// 将 Shape 的 Color 转换为 TextColor
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>),
}

/// 注册自定义字体到全局字体上下文。
///
/// 适用于不需要创建 `LieChart` 实例即可加载字体的场景。
/// 加载后的字体可以通过 `font_family` 名称在图表的文本样式中使用。
///
/// # 示例
///
/// ```ignore
/// // 从内存加载(例如从 CDN 下载的字节)
/// liecharts::register_font(liecharts::FontSource::Memory(font_bytes), Some("MyFont")).unwrap();
/// ```
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(())
}


/// 创建文本布局
/// 
/// 使用 parley 以 **左对齐** 排版文本,返回布局以获取自然宽度/高度。
/// 组件的对齐(居中、右对齐等)应在拿到 layout 尺寸后手动计算位置偏移。
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));

    // 始终左对齐:parley 不做居中/右对齐,组件的对齐由 compute_text_offset 或手动计算实现
    layout.align(Alignment::Start, AlignmentOptions::default());

    layout
}

/// 将多段不同样式的文本合并在一个 TextLayout 中。
///
/// 每段文本可以有自己的 TextStyle(字体、字号、颜色)。
/// 所有文本按顺序直接拼接,通过 parley 的 RangedBuilder 为各段应用不同样式。
/// 需要换行时,请在文本段中自行包含 `\n`。
/// 最终返回单一的 TextLayout,支持断行和多行对齐。
///
/// # 参数
/// - `texts`: 文本段列表,每项为 `(文本内容, 文本样式)`。至少包含一段。
/// - `max_width`: 最大行宽,`None` 表示不断行。
/// - `align`: 多行对齐方式。`Left`、`Center` 或 `Right`。
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)
    })
}


/// 使用指定的 FontContext 和 LayoutContext 创建多段样式文本布局。
///
/// 这是 `layout_text` 的低级版本,适用于需要复用上下文的场景。
/// 直接拼接所有文本段(不带额外分隔符),以第一段的样式为默认样式,
/// 其余各段通过 ranged_builder 的 `push` 方法覆盖特定范围的样式属性。
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);
    }

    // 1. 直接拼接所有文本(不带额外分隔符,用户可在文本中自行添加 \n)
    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));
    }

    // 2. 创建 ranged_builder,以第一段样式作为默认
    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)));

    // 3. 后续各段覆盖样式
    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,
            );
        }
    }

    // 4. 构建布局
    let mut layout = builder.build(&combined);

    // 5. 断行
    layout.break_all_lines(max_width.map(|w| w as f32));

    // 6. 对齐:映射 TextAlign → parley::Alignment
    let paragraph_align = match align {
        TextAlign::Left => Alignment::Start,
        TextAlign::Center => Alignment::Center,
        TextAlign::Right => Alignment::End,
    };
    layout.align(paragraph_align, AlignmentOptions::default());

    layout
}

/// 从锚点到文本块左上角的偏移量
///
/// 根据期望的对齐/基线方式,计算从锚点坐标到文本块左上角的偏移。
/// 组件使用范式:
///
/// ```ignore
/// let layout = create_text_layout(text, &font, max_width);
/// let (x_off, y_off) = compute_text_offset(&layout, align, baseline);
/// let position = Point::new(anchor.x + x_off, anchor.y + y_off);
///
/// // TextRun 始终用 Left/Top(position 已是左上角)
/// VisualElement::TextRun {
///     position,
///     align: TextAlign::Left,
///     baseline: TextBaseline::Top,
///     ...
/// }
/// ```
///
/// 例如:
/// - 锚点=单元格中心,align=Center → x_off = -width/2,position = 左边缘
/// - 锚点=文本中心,baseline=Middle → y_off = -height/2,position = 上边缘
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 => {
            // 对于基线对齐,使用第一行的 ascent
            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()
    }
}