use plotive_base::{Color, ColorU8, color, geom};
use ttf_parser as ttf;
use crate::{Error, font, fontdb, line};
mod boundaries;
mod builder;
mod parse;
mod render;
use boundaries::Boundaries;
pub use parse::{
ParseRichTextError, ParsedRichText, parse_rich_text, parse_rich_text_with_classes,
};
pub use render::{RichPrimitive, render_rich_text, render_rich_text_with};
#[derive(Debug, Clone, Copy, Default)]
pub enum Align {
#[default]
Start,
Center,
End,
Left,
Right,
Justify(f32),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VerAlign {
Line(usize, line::VerAlign),
Top,
Center,
Bottom,
}
impl Default for VerAlign {
fn default() -> Self {
VerAlign::Line(0, Default::default())
}
}
impl From<line::VerAlign> for VerAlign {
fn from(value: line::VerAlign) -> Self {
Self::Line(0, value)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub enum HorAlign {
Left,
#[default]
Center,
Right,
}
#[derive(Debug, Clone, Copy, Default)]
pub enum Direction {
#[default]
Mixed,
MixedLTR,
MixedRTL,
LTR,
RTL,
}
#[derive(Debug, Clone, Copy, Default)]
pub enum VerDirection {
#[default]
TTB,
BTT,
}
#[derive(Debug, Clone, Copy, Default)]
pub enum VerProgression {
#[default]
PerScript,
LTR,
RTL,
}
#[derive(Debug, Clone, Copy)]
pub struct InterColumn(pub f32);
impl Default for InterColumn {
fn default() -> Self {
InterColumn(0.5)
}
}
#[derive(Debug, Clone, Copy)]
pub enum Layout {
Horizontal(Align, VerAlign, Direction),
Vertical(Align, HorAlign, VerDirection, VerProgression, InterColumn),
}
impl Default for Layout {
fn default() -> Self {
Layout::Horizontal(Default::default(), Default::default(), Default::default())
}
}
#[derive(Debug, Clone)]
pub struct RichTextBuilder<C>
where
C: Clone + PartialEq,
{
text: String,
root_props: TextProps<C>,
layout: Layout,
spans: Vec<TextSpan<C>>,
}
impl<C> RichTextBuilder<C>
where
C: Clone + PartialEq,
{
pub fn new(text: String, root_props: TextProps<C>) -> RichTextBuilder<C> {
RichTextBuilder {
text,
root_props,
layout: Layout::default(),
spans: vec![],
}
}
pub fn with_layout(mut self, layout: Layout) -> Self {
self.layout = layout;
self
}
pub fn add_span(&mut self, start: usize, end: usize, props: TextOptProps<C>) {
assert!(start <= end);
assert!(
self.text.is_char_boundary(start) && self.text.is_char_boundary(end),
"start and end must be on char boundaries"
);
self.spans.push(TextSpan { start, end, props });
}
pub fn done(self, fontdb: &fontdb::Database) -> Result<RichText<C>, Error> {
self.done_impl(fontdb)
}
}
#[derive(Debug, Clone)]
pub struct RichText<C = ColorU8>
where
C: Clone,
{
text: String,
layout: Layout,
lines: Vec<LineSpan<C>>,
bbox: Option<geom::Rect>,
}
impl<C> RichText<C>
where
C: Clone,
{
pub fn text(&self) -> &str {
&self.text
}
pub fn layout(&self) -> Layout {
self.layout
}
pub fn lines(&self) -> &[LineSpan<C>] {
&self.lines
}
pub fn bbox(&self) -> Option<&geom::Rect> {
self.bbox.as_ref()
}
#[inline]
pub fn width(&self) -> f32 {
self.bbox.map_or(0.0, |bbox| bbox.width())
}
#[inline]
pub fn height(&self) -> f32 {
self.bbox.map_or(0.0, |bbox| bbox.height())
}
pub fn visual_bbox(&self) -> Option<geom::Rect> {
if self.lines.is_empty() {
return None;
}
let mut bbox = None;
for l in &self.lines {
bbox = geom::Rect::unite_opt(bbox.as_ref(), l.visual_bbox().as_ref());
}
bbox
}
pub fn to_other_color<D, M>(&self, color_map: M) -> RichText<D>
where
D: Clone,
M: Fn(&C) -> D,
{
RichText {
text: self.text.clone(),
layout: self.layout,
lines: self
.lines
.iter()
.map(|l| l.to_other_color(&color_map))
.collect(),
bbox: self.bbox,
}
}
fn empty() -> Self {
Self {
text: String::new(),
layout: Layout::default(),
lines: Vec::new(),
bbox: None,
}
}
#[cfg(debug_assertions)]
pub fn assert_flat_coverage(&self) {
let len = self.text.len();
let mut cursor = 0;
for l in self.lines.iter() {
assert_eq!(l.start, cursor);
cursor = l.end;
if cursor == len {
break;
}
if self.text.as_bytes()[cursor] == b'\r' {
cursor += 1;
}
assert_eq!(
self.text.as_bytes()[cursor],
b'\n',
"expected end of line, found {}",
self.text[cursor..].chars().next().unwrap()
);
cursor += 1;
l.assert_flat_coverage();
}
assert_eq!(cursor, len);
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TextOptProps<C> {
pub font_family: Option<Vec<font::Family>>,
pub font_weight: Option<font::Weight>,
pub font_width: Option<font::Width>,
pub font_style: Option<font::Style>,
pub font_size: Option<f32>,
pub fill: Option<C>,
pub stroke: Option<(C, f32)>,
pub underline: Option<bool>,
pub strikeout: Option<bool>,
}
impl<C> Default for TextOptProps<C> {
fn default() -> Self {
TextOptProps {
font_family: None,
font_weight: None,
font_width: None,
font_style: None,
font_size: None,
fill: None,
stroke: None,
underline: None,
strikeout: None,
}
}
}
impl<C> TextOptProps<C> {
fn affect_shape(&self) -> bool {
self.font_family.is_some()
|| self.font_weight.is_some()
|| self.font_width.is_some()
|| self.font_style.is_some()
|| self.font_size.is_some()
}
}
#[derive(Debug, Clone)]
pub struct TextProps<C>
where
C: Clone,
{
font_size: f32,
font: font::Font,
fill: Option<C>,
outline: Option<(C, f32)>,
underline: bool,
strikeout: bool,
}
impl<C> TextProps<C>
where
C: Clone,
{
pub fn to_other_color<D, M>(&self, color_map: M) -> TextProps<D>
where
D: Clone,
M: Fn(&C) -> D,
{
TextProps {
font_size: self.font_size,
font: self.font.clone(),
fill: self.fill.as_ref().map(|c| color_map(c)),
outline: self.outline.as_ref().map(|(c, w)| (color_map(c), *w)),
underline: self.underline,
strikeout: self.strikeout,
}
}
}
pub trait Foreground {
fn foreground() -> Self;
}
impl Foreground for ColorU8 {
fn foreground() -> Self {
color::BLACK
}
}
impl<C> TextProps<C>
where
C: Color + Foreground,
{
pub fn new(font_size: f32) -> TextProps<C> {
TextProps {
font_size,
font: font::Font::default(),
fill: Some(C::foreground()),
outline: None,
underline: false,
strikeout: false,
}
}
}
impl<C> TextProps<C>
where
C: Clone,
{
pub fn with_font(mut self, font: font::Font) -> Self {
self.font = font;
self
}
pub fn with_fill(mut self, fill: Option<C>) -> Self {
self.fill = fill;
self
}
pub fn with_outline(mut self, stroke: (C, f32)) -> Self {
self.outline = Some(stroke);
self
}
pub fn with_underline(mut self) -> Self {
self.underline = true;
self
}
pub fn with_strikeout(mut self) -> Self {
self.strikeout = true;
self
}
pub fn font_size(&self) -> f32 {
self.font_size
}
pub fn font(&self) -> &font::Font {
&self.font
}
pub fn fill(&self) -> Option<C> {
self.fill.clone()
}
pub fn outline(&self) -> Option<(C, f32)> {
self.outline.clone()
}
pub fn underline(&self) -> bool {
self.underline
}
pub fn strikeout(&self) -> bool {
self.strikeout
}
fn apply_opts(&mut self, opts: &TextOptProps<C>) {
if let Some(font_family) = &opts.font_family {
self.font = self.font.clone().with_families(font_family.clone());
}
if let Some(font_weight) = opts.font_weight {
self.font = self.font.clone().with_weight(font_weight);
}
if let Some(font_width) = opts.font_width {
self.font = self.font.clone().with_width(font_width);
}
if let Some(font_style) = opts.font_style {
self.font = self.font.clone().with_style(font_style);
}
if let Some(font_size) = opts.font_size {
self.font_size = font_size;
}
if let Some(fill) = opts.fill.as_ref() {
self.fill = Some(fill.clone());
}
if let Some(stroke) = opts.stroke.as_ref() {
self.outline = Some(stroke.clone());
}
if let Some(underline) = opts.underline {
self.underline = underline;
}
if let Some(strikeout) = opts.strikeout {
self.strikeout = strikeout;
}
}
}
#[derive(Debug, Clone)]
struct TextSpan<C> {
start: usize,
end: usize,
props: TextOptProps<C>,
}
#[derive(Debug, Clone)]
pub struct LineSpan<C>
where
C: Clone,
{
start: usize,
end: usize,
shapes: Vec<ShapeSpan<C>>,
main_dir: rustybuzz::Direction,
bbox: Option<geom::Rect>,
}
impl<C> LineSpan<C>
where
C: Clone,
{
pub fn to_other_color<D, M>(&self, color_map: M) -> LineSpan<D>
where
D: Clone,
M: Fn(&C) -> D,
{
LineSpan {
start: self.start,
end: self.end,
shapes: self
.shapes
.iter()
.map(|s| s.to_other_color(&color_map))
.collect(),
main_dir: self.main_dir,
bbox: self.bbox,
}
}
pub fn start(&self) -> usize {
self.start
}
pub fn end(&self) -> usize {
self.end
}
pub fn shapes(&self) -> &[ShapeSpan<C>] {
&self.shapes
}
pub fn main_dir(&self) -> rustybuzz::Direction {
self.main_dir
}
pub fn bbox(&self) -> Option<geom::Rect> {
self.bbox
}
pub fn total_height(&self) -> f32 {
self.height() + self.gap()
}
pub fn gap(&self) -> f32 {
self.shapes
.iter()
.map(|s| s.metrics.line_gap)
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap_or(0.0)
}
pub fn height(&self) -> f32 {
self.shapes
.iter()
.map(|s| s.metrics.height())
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap_or(0.0)
}
pub fn ascent(&self) -> f32 {
self.shapes
.iter()
.map(|s| s.metrics.ascent)
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap_or(0.0)
}
pub fn descent(&self) -> f32 {
self.shapes
.iter()
.map(|s| s.metrics.descent)
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap_or(0.0)
}
pub fn cap_height(&self) -> f32 {
self.shapes
.iter()
.map(|s| s.metrics.cap_height)
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap_or(0.0)
}
pub fn x_height(&self) -> f32 {
if self.shapes.is_empty() {
return 0.0;
}
let sum: f32 = self.shapes.iter().map(|s| s.metrics.x_height).sum();
sum / (self.shapes.len() as f32)
}
pub fn visual_bbox(&self) -> Option<geom::Rect> {
if self.shapes.is_empty() {
return None;
}
let mut bbox = None;
for s in &self.shapes {
bbox = geom::Rect::unite_opt(bbox.as_ref(), Some(&s.visual_bbox()));
}
bbox
}
#[cfg(debug_assertions)]
fn assert_flat_coverage(&self) {
let mut cursor = self.start;
for s in self.shapes.iter() {
assert_eq!(s.start, cursor);
cursor = s.end;
s.assert_flat_coverage();
}
assert_eq!(cursor, self.end);
}
}
#[derive(Debug, Clone)]
pub struct ShapeSpan<C>
where
C: Clone,
{
start: usize,
end: usize,
spans: Vec<PropsSpan<C>>,
face_id: fontdb::ID,
glyphs: Vec<Glyph>,
metrics: font::ScaledMetrics,
y_baseline: f32,
bbox: Option<geom::Rect>,
}
impl<C> ShapeSpan<C>
where
C: Clone,
{
pub fn to_other_color<D, M>(&self, color_map: M) -> ShapeSpan<D>
where
D: Clone,
M: Fn(&C) -> D,
{
ShapeSpan {
start: self.start,
end: self.end,
spans: self
.spans
.iter()
.map(|s| s.to_other_color(&color_map))
.collect(),
face_id: self.face_id,
glyphs: self.glyphs.clone(),
metrics: self.metrics,
y_baseline: self.y_baseline,
bbox: self.bbox,
}
}
pub fn start(&self) -> usize {
self.start
}
pub fn end(&self) -> usize {
self.end
}
pub fn font(&self) -> &font::Font {
&self.spans[0].props.font
}
pub fn font_size(&self) -> f32 {
self.spans[0].props.font_size
}
pub fn spans(&self) -> &[PropsSpan<C>] {
&self.spans
}
pub fn metrics(&self) -> font::ScaledMetrics {
self.metrics
}
pub fn bbox(&self) -> geom::Rect {
self.bbox.unwrap()
}
pub fn visual_bbox(&self) -> geom::Rect {
assert!(!self.glyphs.is_empty());
let mut bbox = None;
for g in &self.glyphs {
match bbox {
Some(ref mut b) => {
*b = geom::Rect::unite(b, &g.visual_bbox());
}
None => {
bbox = Some(g.visual_bbox());
}
}
}
bbox.unwrap()
}
#[cfg(debug_assertions)]
fn assert_flat_coverage(&self) {
let mut cursor = self.start;
for s in self.spans.iter() {
assert_eq!(s.start, cursor);
cursor = s.end;
}
assert_eq!(cursor, self.end);
}
}
#[derive(Debug, Clone)]
pub struct PropsSpan<C>
where
C: Clone,
{
start: usize,
end: usize,
props: TextProps<C>,
bbox: Option<geom::Rect>,
}
impl<C> PropsSpan<C>
where
C: Clone,
{
pub fn to_other_color<D, M>(&self, color_map: M) -> PropsSpan<D>
where
D: Clone,
M: Fn(&C) -> D,
{
PropsSpan {
start: self.start,
end: self.end,
props: self.props.to_other_color(color_map),
bbox: self.bbox,
}
}
pub fn start(&self) -> usize {
self.start
}
pub fn end(&self) -> usize {
self.end
}
pub fn props(&self) -> &TextProps<C> {
&self.props
}
pub fn bbox(&self) -> geom::Rect {
self.bbox.unwrap()
}
}
#[derive(Debug, Clone, Copy)]
struct Glyph {
id: ttf::GlyphId,
cluster: usize,
x_advance: f32,
y_advance: f32,
x_offset: f32,
y_offset: f32,
ts: tiny_skia::Transform,
rect: ttf::Rect,
}
impl Glyph {
fn visual_bbox(&self) -> geom::Rect {
let mut tl_br = [
geom::Point {
x: self.rect.x_min as f32,
y: self.rect.y_max as f32,
},
geom::Point {
x: self.rect.x_max as f32,
y: self.rect.y_min as f32,
},
];
self.ts.map_points(&mut tl_br);
geom::Rect::from_trbl(tl_br[0].y, tl_br[1].x, tl_br[1].y, tl_br[0].x)
}
}