use crate::{svgr, FFramesContext, FontSource, FontStretch, FontStyle, FontVariant, Svgr};
use lru::LruCache;
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
sync::{Arc, Mutex},
};
use usvgr::svgtree::SvgAttributeValue;
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum TextAlign {
Left,
Center,
Right,
}
#[derive(Debug, Clone, Copy)]
pub struct BreakLinesOpts<
'a,
TX: Into<SvgAttributeValue<'a>> + Hash + Default,
TY: Into<SvgAttributeValue<'a>> + Hash + Default,
> {
pub width: usize,
pub line_height: f32,
pub font_family: &'a str,
pub font_size: usize,
pub x: TX,
pub y: TY,
pub align: TextAlign,
pub font_weight: u16,
pub fill: &'a str,
pub font_style: FontStyle,
pub font_stretch: FontStretch,
pub dominant_baseline: &'a str,
pub text_anchor: &'a str,
}
impl<'a, TX, TY> BreakLinesOpts<'a, TX, TY>
where
TX: Into<SvgAttributeValue<'a>> + Hash + Default,
TY: Into<SvgAttributeValue<'a>> + Hash + Default,
{
pub(crate) fn hash_with_value(&self, value: &str) -> u64 {
let mut s = DefaultHasher::new();
value.hash(&mut s);
self.hash(&mut s);
s.finish()
}
pub fn create_text_svgr(self, children: Svgr<'a>) -> Svgr<'a> {
let BreakLinesOpts::<'a, TX, TY> {
font_family,
font_size,
font_weight,
x,
y,
fill,
dominant_baseline,
text_anchor,
font_stretch,
..
} = self;
svgr!(
<text
x={x.into()}
y={y.into()}
fill={fill}
font-size={font_size}
font-family={font_family}
font-weight={font_weight}
font-stretch={font_stretch.to_string()}
dominant-baseline={dominant_baseline}
text-anchor={text_anchor}
>
{children}
</text>
)
}
}
impl<'a, TX, TY> std::hash::Hash for BreakLinesOpts<'a, TX, TY>
where
TX: Into<SvgAttributeValue<'a>> + Hash + Default,
TY: Into<SvgAttributeValue<'a>> + Hash + Default,
{
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.width.hash(state);
self.line_height.to_bits().hash(state);
self.font_size.hash(state);
self.x.hash(state);
self.y.hash(state);
self.align.hash(state);
self.font_weight.hash(state);
self.fill.hash(state);
self.font_family.hash(state);
self.dominant_baseline.hash(state);
self.text_anchor.hash(state);
}
}
impl<'a, TX, TY> Default for BreakLinesOpts<'a, TX, TY>
where
TX: Into<SvgAttributeValue<'a>> + Hash + Default,
TY: Into<SvgAttributeValue<'a>> + Hash + Default,
{
fn default() -> Self {
Self {
width: 0,
line_height: 1.1,
font_family: Default::default(),
font_size: 16,
x: TX::default(),
y: TY::default(),
fill: "",
align: TextAlign::Left,
font_style: Default::default(),
font_stretch: Default::default(),
font_weight: 400,
dominant_baseline: "auto",
text_anchor: "start",
}
}
}
#[derive(Debug, Clone)]
pub struct BreaksLruCache(pub(crate) Arc<Mutex<LruCache<u64, Option<WrappedTextStructure>>>>);
impl BreaksLruCache {
pub fn new(size: usize) -> Option<Self> {
if size > 0 {
Some(Self(Arc::new(Mutex::new(lru::LruCache::new(
std::num::NonZeroUsize::new(size)
.unwrap_or(std::num::NonZeroUsize::new(1).unwrap()),
)))))
} else {
None
}
}
}
#[derive(Debug, Clone)]
pub struct WrappedTextLine {
pub words: Vec<String>,
pub width: usize,
pub dx: usize,
pub dy: usize,
}
#[derive(Debug, Clone, Default)]
pub struct WrappedTextStructure {
pub lines: Vec<WrappedTextLine>,
pub(crate) line_height_in_px: f32,
pub(crate) hash: u64,
}
impl WrappedTextStructure {
pub fn as_svgr<
'a,
X: Into<SvgAttributeValue<'a>> + Hash + Default,
Y: Into<SvgAttributeValue<'a>> + Hash + Default,
>(
&self,
BreakLinesOpts {
x,
y,
fill,
font_size,
font_family,
font_weight,
dominant_baseline,
text_anchor,
..
}: BreakLinesOpts<'a, X, Y>,
) -> Svgr<'a> {
let svgr_x: SvgAttributeValue = x.into();
let svgr_y: SvgAttributeValue = y.into();
svgr_macro::svgr! {
<text
id={self.hash}
x={svgr_x.clone()}
y={svgr_y.clone()}
fill={fill}
font-size={font_size}
font-family={font_family}
font-weight={font_weight}
dominant-baseline={dominant_baseline}
text-anchor={text_anchor}
>
{Svgr::from_iter(
self.lines.iter().map(|line| {
svgr_macro::svgr! {
<tspan
x={svgr_x.clone()}
y={svgr_y.clone()}
dx={line.dx}
dy={line.dy}
>
{line.words.join(" ")}
</tspan>
}
})
)}
</text>
}
}
pub fn occupied_height(&self) -> usize {
let lines = &self.lines;
if lines.is_empty() {
return 0;
}
if lines.len() == 1 {
return self.line_height_in_px as usize;
}
lines.len() * self.line_height_in_px as usize
}
}
pub(crate) fn text_wrap_impl<
'a,
'b,
TX: Into<SvgAttributeValue<'a>> + Hash + Default,
TY: Into<SvgAttributeValue<'a>> + Hash + Default,
>(
hash: u64,
value: &'b str,
font_source: &'a (dyn FontSource<'a> + 'a),
options: BreakLinesOpts<'a, TX, TY>,
) -> Option<WrappedTextStructure> {
let BreakLinesOpts {
width,
align,
font_family,
font_size,
font_weight,
font_style,
font_stretch,
line_height,
..
} = options;
let font_face = if let Some(font_face) =
font_source.resolve_font(font_family, font_weight, font_style, font_stretch)
{
font_face
} else {
crate::log!("ERROR breaking lines: font is not resolved. Make sure that system fonts are not available for break_lines feature in editor.");
return None;
};
let font_variant = font_face.font_variant(font_size)?;
let space_width = match font_variant {
crate::FontVariant::Monospaced(mono_width) => mono_width,
crate::FontVariant::Variable => font_face.resolve_char_width(font_size, ' ')?,
};
let resolve_word_width = |word: &str| -> Option<usize> {
let raw_width = match font_variant {
crate::FontVariant::Monospaced(mono_width) => word.len() * mono_width,
crate::FontVariant::Variable => word
.chars()
.filter_map(|char| font_face.resolve_char_width(font_size, char))
.sum(),
};
Some(raw_width)
};
let mut structure = vec![(vec![], 0usize)];
let line_height_in_px = line_height * font_size as f32;
for word in value.split_whitespace() {
let word_width = resolve_word_width(word)?;
let (last_line, last_line_width) = structure.last_mut()?;
if *last_line_width + space_width + word_width > width {
structure.push((vec![word.to_owned()], word_width));
} else {
if *last_line_width != 0 {
*last_line_width += space_width;
}
last_line.push(word.to_owned());
*last_line_width += word_width;
}
}
let lines = structure
.into_iter()
.enumerate()
.map(|(index, (words, line_width))| {
let dx = match align {
TextAlign::Left => 0,
TextAlign::Center => (width - line_width) / 2,
TextAlign::Right => width - line_width,
};
WrappedTextLine {
words,
width: line_width,
dx,
dy: (index as f32 * line_height_in_px) as usize,
}
})
.collect();
Some(WrappedTextStructure {
lines,
line_height_in_px,
hash,
})
}
#[derive(Debug, Clone)]
pub struct EstimateTextWidthOptions<'a> {
pub font_family: &'a str,
pub font_size: usize,
pub font_weight: u16,
pub font_style: FontStyle,
pub font_stretch: FontStretch,
}
pub fn estimate_text_width<'a>(
ctx: &FFramesContext<'a, '_>,
text: &'a str,
options: EstimateTextWidthOptions<'a>,
) -> Option<usize> {
let EstimateTextWidthOptions {
font_family,
font_size,
font_weight,
font_style,
font_stretch,
} = options;
let font_face =
ctx.font_source?
.resolve_font(font_family, font_weight, font_style, font_stretch)?;
let font_variant = font_face.font_variant(font_size)?;
match font_variant {
FontVariant::Monospaced(mono_width) => Some(text.len() * mono_width),
FontVariant::Variable => {
let total_width = text
.chars()
.filter_map(|char| font_face.resolve_char_width(font_size, char))
.sum();
Some(total_width)
}
}
}