use super::bidi;
use super::font::FaceEntry;
use super::sdf::{SDF_EM_PX, SDF_Y_OFFSET_PX};
use super::shape::{shape_sections, ShapeSection, ShapedGlyph, ShapedText};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Anchor {
#[default]
Center,
Left,
Right,
Top,
Bottom,
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}
impl Anchor {
pub fn parse(s: &str) -> Option<Anchor> {
Some(match s {
"center" => Anchor::Center,
"left" => Anchor::Left,
"right" => Anchor::Right,
"top" => Anchor::Top,
"bottom" => Anchor::Bottom,
"top-left" => Anchor::TopLeft,
"top-right" => Anchor::TopRight,
"bottom-left" => Anchor::BottomLeft,
"bottom-right" => Anchor::BottomRight,
_ => return None,
})
}
pub fn fraction(self) -> (f32, f32) {
match self {
Anchor::Center => (0.5, 0.5),
Anchor::Left => (0.0, 0.5),
Anchor::Right => (1.0, 0.5),
Anchor::Top => (0.5, 0.0),
Anchor::Bottom => (0.5, 1.0),
Anchor::TopLeft => (0.0, 0.0),
Anchor::TopRight => (1.0, 0.0),
Anchor::BottomLeft => (0.0, 1.0),
Anchor::BottomRight => (1.0, 1.0),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Justify {
#[default]
Auto,
Left,
Center,
Right,
}
impl Justify {
pub fn parse(s: &str) -> Option<Justify> {
Some(match s {
"auto" => Justify::Auto,
"left" => Justify::Left,
"center" => Justify::Center,
"right" => Justify::Right,
_ => return None,
})
}
fn fraction(self, anchor: Anchor) -> f32 {
match self {
Justify::Left => 0.0,
Justify::Center => 0.5,
Justify::Right => 1.0,
Justify::Auto => match anchor {
Anchor::Left | Anchor::TopLeft | Anchor::BottomLeft => 0.0,
Anchor::Right | Anchor::TopRight | Anchor::BottomRight => 1.0,
_ => 0.5,
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TextTransform {
#[default]
None,
Uppercase,
Lowercase,
}
impl TextTransform {
pub fn parse(s: &str) -> Option<TextTransform> {
Some(match s {
"none" => TextTransform::None,
"uppercase" => TextTransform::Uppercase,
"lowercase" => TextTransform::Lowercase,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct LayoutParams {
pub max_width_em: f32,
pub line_height_em: f32,
pub letter_spacing_em: f32,
pub anchor: Anchor,
pub justify: Justify,
pub offset_em: [f32; 2],
pub transform: TextTransform,
}
impl Default for LayoutParams {
fn default() -> Self {
LayoutParams {
max_width_em: 10.0,
line_height_em: 1.2,
letter_spacing_em: 0.0,
anchor: Anchor::Center,
justify: Justify::Auto,
offset_em: [0.0, 0.0],
transform: TextTransform::None,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct PlacedGlyph {
pub font: usize,
pub glyph_id: u16,
pub x: f32,
pub y: f32,
pub advance: f32,
pub scale: f32,
pub section: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum VerticalAlign {
#[default]
Baseline,
Top,
Center,
Bottom,
}
impl VerticalAlign {
pub fn parse(s: &str) -> Option<VerticalAlign> {
Some(match s {
"baseline" => VerticalAlign::Baseline,
"top" | "text-top" => VerticalAlign::Top,
"center" => VerticalAlign::Center,
"bottom" | "text-bottom" => VerticalAlign::Bottom,
_ => return None,
})
}
}
#[derive(Debug, Clone)]
pub struct SectionSpec<'a> {
pub text: &'a str,
pub fonts: std::ops::Range<usize>,
pub scale: f32,
pub valign: VerticalAlign,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct EmBox {
pub min_x: f32,
pub min_y: f32,
pub max_x: f32,
pub max_y: f32,
}
impl EmBox {
pub fn width(&self) -> f32 {
self.max_x - self.min_x
}
pub fn height(&self) -> f32 {
self.max_y - self.min_y
}
}
#[derive(Debug, Default)]
pub struct TextBlock {
pub glyphs: Vec<PlacedGlyph>,
pub bbox: EmBox,
pub dropped_chars: usize,
pub missing_range_chars: usize,
}
impl TextBlock {
pub fn is_empty(&self) -> bool {
self.glyphs.is_empty()
}
}
pub fn layout(text: &str, fonts: &[FaceEntry<'_>], params: &LayoutParams) -> TextBlock {
layout_sections(
&[SectionSpec {
text,
fonts: 0..fonts.len(),
scale: 1.0,
valign: VerticalAlign::Baseline,
}],
fonts,
params,
)
}
pub fn layout_sections(
sections: &[SectionSpec<'_>],
fonts: &[FaceEntry<'_>],
params: &LayoutParams,
) -> TextBlock {
let (Some(primary), false) = (fonts.first(), sections.iter().all(|s| s.text.is_empty())) else {
return TextBlock::default();
};
let transformed: Vec<String> = sections
.iter()
.map(|s| match params.transform {
TextTransform::None => s.text.to_string(),
TextTransform::Uppercase => s.text.to_uppercase(),
TextTransform::Lowercase => s.text.to_lowercase(),
})
.collect();
let shape_secs: Vec<ShapeSection<'_>> = sections
.iter()
.zip(&transformed)
.map(|(s, t)| ShapeSection {
text: t,
fonts: s.fonts.clone(),
scale: s.scale,
})
.collect();
let shaped = shape_sections(&shape_secs, fonts, params.letter_spacing_em);
if shaped.glyphs.is_empty() {
return TextBlock {
dropped_chars: shaped.dropped,
missing_range_chars: shaped.missing_range,
..TextBlock::default()
};
}
let breaks = determine_line_breaks(&shaped, params.max_width_em);
let lines = split_lines(&shaped, &breaks);
let lh = params.line_height_em;
let (base_asc, base_desc) = match primary {
FaceEntry::Outline { font, .. } => (font.ascent_em(), font.descent_em()),
FaceEntry::Sdf(_) => {
let asc = 0.5 * lh + SDF_Y_OFFSET_PX / SDF_EM_PX;
(asc, lh - asc)
}
};
let line_scale: Vec<f32> = lines
.iter()
.map(|l| {
l.glyphs
.iter()
.map(|g| g.scale)
.reduce(f32::max)
.unwrap_or(1.0)
})
.collect();
let mut baselines = Vec::with_capacity(lines.len());
for i in 0..lines.len() {
let b = if i == 0 {
base_asc * line_scale[0]
} else {
baselines[i - 1] + lh * line_scale[i - 1].max(line_scale[i])
};
baselines.push(b);
}
let last = lines.len() - 1;
let block_h = baselines[last] + base_desc * line_scale[last];
let block_w = lines.iter().map(|l| l.width).fold(0.0f32, f32::max);
let justify = params.justify.fraction(params.anchor);
let (ax, ay) = params.anchor.fraction();
let shift_x = -ax * block_w + params.offset_em[0];
let shift_y = -ay * block_h + params.offset_em[1];
let mut glyphs = Vec::new();
for (line_ix, line) in lines.iter().enumerate() {
let s_line = line_scale[line_ix];
let line_x = (block_w - line.width) * justify + shift_x;
let baseline = baselines[line_ix] + shift_y;
let mut pen = 0.0f32;
for g in &line.glyphs {
let valign = sections
.get(g.section as usize)
.map(|s| s.valign)
.unwrap_or_default();
let dy = valign_shift(valign, base_asc, base_desc, s_line, g.scale);
glyphs.push(PlacedGlyph {
font: g.font,
glyph_id: g.glyph_id,
x: line_x + pen + g.x_offset,
y: baseline + dy - g.y_offset,
advance: g.x_advance,
scale: g.scale,
section: g.section,
});
pen += g.x_advance;
}
}
TextBlock {
glyphs,
bbox: EmBox {
min_x: shift_x,
min_y: shift_y,
max_x: shift_x + block_w,
max_y: shift_y + block_h,
},
dropped_chars: shaped.dropped,
missing_range_chars: shaped.missing_range,
}
}
fn valign_shift(v: VerticalAlign, base_asc: f32, base_desc: f32, s_line: f32, s_g: f32) -> f32 {
let (asc_line, desc_line) = (base_asc * s_line, base_desc * s_line);
let (asc_g, desc_g) = (base_asc * s_g, base_desc * s_g);
match v {
VerticalAlign::Baseline => 0.0,
VerticalAlign::Bottom => desc_line - desc_g,
VerticalAlign::Top => -(asc_line - asc_g),
VerticalAlign::Center => ((desc_line - desc_g) - (asc_line - asc_g)) / 2.0,
}
}
struct Line<'a> {
glyphs: Vec<&'a ShapedGlyph>,
width: f32,
}
fn split_lines<'a>(shaped: &'a ShapedText, breaks: &[usize]) -> Vec<Line<'a>> {
let mut lines = Vec::new();
let mut glyph_start = 0usize;
for &brk in breaks {
let glyph_end = shaped.glyphs[glyph_start..]
.iter()
.position(|g| g.char_ix >= brk)
.map(|p| glyph_start + p)
.unwrap_or(shaped.glyphs.len());
let mut slice = &shaped.glyphs[glyph_start..glyph_end];
while let Some(g) = slice.first() {
if !is_whitespace(shaped.chars[g.char_ix]) {
break;
}
slice = &slice[1..];
}
while let Some(g) = slice.last() {
if !is_whitespace(shaped.chars[g.char_ix]) {
break;
}
slice = &slice[..slice.len() - 1];
}
let mut glyphs: Vec<&ShapedGlyph> = slice.iter().collect();
bidi::reorder_visual(&mut glyphs, |g| g.level);
lines.push(Line {
width: glyphs.iter().map(|g| g.x_advance).sum(),
glyphs,
});
glyph_start = glyph_end;
}
lines
}
struct BreakCandidate {
char_ix: usize,
x: f32,
prior: Option<usize>,
badness: f64,
}
fn determine_line_breaks(shaped: &ShapedText, max_width_em: f32) -> Vec<usize> {
let end = shaped.chars.len();
let total: f32 = shaped.glyphs.iter().map(|g| g.x_advance).sum();
let target = if max_width_em > 0.0 {
total / (total / max_width_em).ceil().max(1.0)
} else {
total
};
let has_zwsp = shaped.chars.contains(&'\u{200b}');
let mut advance = vec![0.0f32; end];
let mut glyph_start = vec![false; end];
for g in &shaped.glyphs {
advance[g.char_ix] += g.x_advance;
glyph_start[g.char_ix] = true;
}
let mut candidates: Vec<BreakCandidate> = Vec::new();
let mut current_x = 0.0f32;
for (i, (&c, &adv)) in shaped.chars.iter().zip(&advance).enumerate() {
if !is_whitespace(c) {
current_x += adv;
}
let next_ix = i + 1;
if next_ix >= end {
break;
}
if !glyph_start[next_ix] && shaped.covered[next_ix] {
continue;
}
let ideographic = char_allows_ideographic_breaking(c);
if is_breakable(c) || ideographic {
let penalty =
calculate_penalty(c, Some(shaped.chars[next_ix]), ideographic && has_zwsp);
let cand = evaluate_break(next_ix, current_x, target, &candidates, penalty, false);
candidates.push(cand);
}
}
let last = evaluate_break(end, current_x, target, &candidates, 0.0, true);
least_bad_breaks(&last, &candidates)
}
fn calculate_badness(line_width: f32, target: f32, penalty: f32, is_last: bool) -> f64 {
let raggedness = f64::from(line_width - target).powi(2);
let penalty = f64::from(penalty);
if is_last && line_width < target {
return raggedness / 2.0;
}
raggedness + penalty.abs() * penalty
}
fn calculate_penalty(c: char, next: Option<char>, penalizable_ideographic: bool) -> f32 {
let mut penalty = 0.0f32;
if c == '\n' {
penalty -= 10000.0;
}
if penalizable_ideographic {
penalty += 150.0;
}
if c == '(' || c == '\u{ff08}' {
penalty += 50.0;
}
if next == Some(')') || next == Some('\u{ff09}') {
penalty += 50.0;
}
penalty
}
fn evaluate_break(
char_ix: usize,
x: f32,
target: f32,
candidates: &[BreakCandidate],
penalty: f32,
is_last: bool,
) -> BreakCandidate {
let mut best_prior = None;
let mut best_badness = calculate_badness(x, target, penalty, is_last);
for (ix, prior) in candidates.iter().enumerate() {
let line_width = x - prior.x;
let badness = calculate_badness(line_width, target, penalty, is_last) + prior.badness;
if badness <= best_badness {
best_prior = Some(ix);
best_badness = badness;
}
}
BreakCandidate {
char_ix,
x,
prior: best_prior,
badness: best_badness,
}
}
fn least_bad_breaks(last: &BreakCandidate, candidates: &[BreakCandidate]) -> Vec<usize> {
let mut breaks = vec![last.char_ix];
let mut prior = last.prior;
while let Some(ix) = prior {
breaks.push(candidates[ix].char_ix);
prior = candidates[ix].prior;
}
breaks.reverse();
breaks
}
fn is_whitespace(c: char) -> bool {
matches!(c, '\t' | '\n' | '\u{b}' | '\u{c}' | '\r' | ' ')
}
fn is_breakable(c: char) -> bool {
matches!(
c,
'\n' | ' '
| '&'
| '('
| ')'
| '+'
| '-'
| '\u{ad}' | '\u{b7}' | '\u{200b}' | '\u{2010}' | '\u{2013}' | '\u{2027}' | '/'
)
}
pub fn char_allows_ideographic_breaking(c: char) -> bool {
let u = c as u32;
if u < 0x2e80 {
return false;
}
matches!(
u,
0x2e80..=0x2eff | 0x2f00..=0x2fdf | 0x2ff0..=0x2fff | 0x3000..=0x303f | 0x3040..=0x309f | 0x30a0..=0x30ff | 0x3100..=0x312f | 0x31a0..=0x31bf | 0x31c0..=0x31ef | 0x31f0..=0x31ff | 0x3200..=0x32ff | 0x3300..=0x33ff | 0x3400..=0x4dbf | 0x4e00..=0x9fff | 0xa000..=0xa48f | 0xa490..=0xa4cf | 0xf900..=0xfaff | 0xfe10..=0xfe1f | 0xfe30..=0xfe4f | 0xff00..=0xffef )
}