use std::borrow::Cow;
use std::fmt::{self, Debug, Formatter};
use std::str::FromStr;
use std::sync::Arc;
use az::SaturatingAs;
use ecow::EcoString;
use rustybuzz::{BufferFlags, ShapePlan, UnicodeBuffer};
use ttf_parser::Tag;
use unicode_bidi::{BidiInfo, Level as BidiLevel};
use unicode_script::{Script, UnicodeScript};
use super::{Item, Range, SpanMapper};
use crate::engine::Engine;
use crate::foundations::{Smart, StyleChain};
use crate::layout::{Abs, Dir, Em, Frame, FrameItem, Point, Size};
use crate::text::{
decorate, families, features, is_default_ignorable, variant, Font, FontVariant,
Glyph, Lang, Region, TextEdgeBounds, TextElem, TextItem,
};
use crate::utils::SliceExt;
use crate::World;
#[derive(Clone)]
pub struct ShapedText<'a> {
pub base: usize,
pub text: &'a str,
pub dir: Dir,
pub lang: Lang,
pub region: Option<Region>,
pub styles: StyleChain<'a>,
pub variant: FontVariant,
pub size: Abs,
pub width: Abs,
pub glyphs: Cow<'a, [ShapedGlyph]>,
}
#[derive(Debug, Clone)]
pub struct ShapedGlyph {
pub font: Font,
pub glyph_id: u16,
pub x_advance: Em,
pub x_offset: Em,
pub y_offset: Em,
pub adjustability: Adjustability,
pub range: Range,
pub safe_to_break: bool,
pub c: char,
pub is_justifiable: bool,
pub script: Script,
}
#[derive(Debug, Clone, Default)]
pub struct Adjustability {
pub stretchability: (Em, Em),
pub shrinkability: (Em, Em),
}
impl ShapedGlyph {
pub fn is_space(&self) -> bool {
is_space(self.c)
}
pub fn is_justifiable(&self) -> bool {
self.is_justifiable
}
pub fn is_cj_script(&self) -> bool {
is_cj_script(self.c, self.script)
}
pub fn is_cjk_punctuation(&self) -> bool {
self.is_cjk_left_aligned_punctuation(CjkPunctStyle::Gb)
|| self.is_cjk_right_aligned_punctuation()
|| self.is_cjk_center_aligned_punctuation(CjkPunctStyle::Gb)
}
pub fn is_cjk_left_aligned_punctuation(&self, style: CjkPunctStyle) -> bool {
is_cjk_left_aligned_punctuation(
self.c,
self.x_advance,
self.stretchability(),
style,
)
}
pub fn is_cjk_right_aligned_punctuation(&self) -> bool {
is_cjk_right_aligned_punctuation(self.c, self.x_advance, self.stretchability())
}
pub fn is_cjk_center_aligned_punctuation(&self, style: CjkPunctStyle) -> bool {
is_cjk_center_aligned_punctuation(self.c, style)
}
pub fn is_letter_or_number(&self) -> bool {
matches!(self.c.script(), Script::Latin | Script::Greek | Script::Cyrillic)
|| matches!(self.c, '#' | '$' | '%' | '&')
|| self.c.is_ascii_digit()
}
pub fn base_adjustability(&self, style: CjkPunctStyle) -> Adjustability {
let width = self.x_advance;
if self.is_space() {
Adjustability {
stretchability: (Em::zero(), width / 2.0),
shrinkability: (Em::zero(), width / 3.0),
}
} else if self.is_cjk_left_aligned_punctuation(style) {
Adjustability {
stretchability: (Em::zero(), Em::zero()),
shrinkability: (Em::zero(), width / 2.0),
}
} else if self.is_cjk_right_aligned_punctuation() {
Adjustability {
stretchability: (Em::zero(), Em::zero()),
shrinkability: (width / 2.0, Em::zero()),
}
} else if self.is_cjk_center_aligned_punctuation(style) {
Adjustability {
stretchability: (Em::zero(), Em::zero()),
shrinkability: (width / 4.0, width / 4.0),
}
} else {
Adjustability::default()
}
}
pub fn stretchability(&self) -> (Em, Em) {
self.adjustability.stretchability
}
pub fn shrinkability(&self) -> (Em, Em) {
self.adjustability.shrinkability
}
pub fn shrink_left(&mut self, amount: Em) {
self.x_offset -= amount;
self.x_advance -= amount;
self.adjustability.shrinkability.0 -= amount;
}
pub fn shrink_right(&mut self, amount: Em) {
self.x_advance -= amount;
self.adjustability.shrinkability.1 -= amount;
}
}
enum Side {
Left,
Right,
}
impl<'a> ShapedText<'a> {
pub fn build(
&self,
engine: &Engine,
spans: &SpanMapper,
justification_ratio: f64,
extra_justification: Abs,
) -> Frame {
let (top, bottom) = self.measure(engine);
let size = Size::new(self.width, top + bottom);
let mut offset = Abs::zero();
let mut frame = Frame::soft(size);
frame.set_baseline(top);
let shift = TextElem::baseline_in(self.styles);
let decos = TextElem::deco_in(self.styles);
let fill = TextElem::fill_in(self.styles);
let stroke = TextElem::stroke_in(self.styles);
let span_offset = TextElem::span_offset_in(self.styles);
for ((font, y_offset), group) in
self.glyphs.as_ref().group_by_key(|g| (g.font.clone(), g.y_offset))
{
let mut range = group[0].range.clone();
for glyph in group {
range.start = range.start.min(glyph.range.start);
range.end = range.end.max(glyph.range.end);
}
let pos = Point::new(offset, top + shift - y_offset.at(self.size));
let glyphs: Vec<Glyph> = group
.iter()
.map(|shaped: &ShapedGlyph| {
let adjustability_left = if justification_ratio < 0.0 {
shaped.shrinkability().0
} else {
shaped.stretchability().0
};
let adjustability_right = if justification_ratio < 0.0 {
shaped.shrinkability().1
} else {
shaped.stretchability().1
};
let justification_left = adjustability_left * justification_ratio;
let mut justification_right =
adjustability_right * justification_ratio;
if shaped.is_justifiable() {
justification_right +=
Em::from_length(extra_justification, self.size)
}
frame.size_mut().x += justification_left.at(self.size)
+ justification_right.at(self.size);
let mut span = spans.span_at(shaped.range.start);
span.1 = span.1.saturating_add(span_offset.saturating_as());
Glyph {
id: shaped.glyph_id,
x_advance: shaped.x_advance
+ justification_left
+ justification_right,
x_offset: shaped.x_offset + justification_left,
range: (shaped.range.start - range.start).saturating_as()
..(shaped.range.end - range.start).saturating_as(),
span,
}
})
.collect();
let item = TextItem {
font,
size: self.size,
lang: self.lang,
region: self.region,
fill: fill.clone(),
stroke: stroke.clone().map(|s| s.unwrap_or_default()),
text: self.text[range.start - self.base..range.end - self.base].into(),
glyphs,
};
let width = item.width();
if decos.is_empty() {
frame.push(pos, FrameItem::Text(item));
} else {
frame.push(pos, FrameItem::Text(item.clone()));
for deco in &decos {
decorate(&mut frame, deco, &item, width, shift, pos);
}
}
offset += width;
}
frame
}
pub fn measure(&self, engine: &Engine) -> (Abs, Abs) {
let mut top = Abs::zero();
let mut bottom = Abs::zero();
let top_edge = TextElem::top_edge_in(self.styles);
let bottom_edge = TextElem::bottom_edge_in(self.styles);
let mut expand = |font: &Font, bounds: TextEdgeBounds| {
let (t, b) = font.edges(top_edge, bottom_edge, self.size, bounds);
top.set_max(t);
bottom.set_max(b);
};
if self.glyphs.is_empty() {
let world = engine.world;
for family in families(self.styles) {
if let Some(font) = world
.book()
.select(family, self.variant)
.and_then(|id| world.font(id))
{
expand(&font, TextEdgeBounds::Zero);
break;
}
}
} else {
for g in self.glyphs.iter() {
expand(&g.font, TextEdgeBounds::Glyph(g.glyph_id));
}
}
(top, bottom)
}
pub fn justifiables(&self) -> usize {
self.glyphs.iter().filter(|g| g.is_justifiable()).count()
}
pub fn cjk_justifiable_at_last(&self) -> bool {
self.glyphs
.last()
.map(|g| g.is_cj_script() || g.is_cjk_punctuation())
.unwrap_or(false)
}
pub fn stretchability(&self) -> Abs {
self.glyphs
.iter()
.map(|g| g.stretchability().0 + g.stretchability().1)
.sum::<Em>()
.at(self.size)
}
pub fn shrinkability(&self) -> Abs {
self.glyphs
.iter()
.map(|g| g.shrinkability().0 + g.shrinkability().1)
.sum::<Em>()
.at(self.size)
}
pub fn reshape(&'a self, engine: &Engine, text_range: Range) -> ShapedText<'a> {
let text = &self.text[text_range.start - self.base..text_range.end - self.base];
if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
#[cfg(debug_assertions)]
assert_all_glyphs_in_range(glyphs, text, text_range.clone());
Self {
base: text_range.start,
text,
dir: self.dir,
lang: self.lang,
region: self.region,
styles: self.styles,
size: self.size,
variant: self.variant,
width: glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(self.size),
glyphs: Cow::Borrowed(glyphs),
}
} else {
shape(
engine,
text_range.start,
text,
self.styles,
self.dir,
self.lang,
self.region,
)
}
}
pub fn empty(&self) -> Self {
Self {
text: "",
width: Abs::zero(),
glyphs: Cow::Borrowed(&[]),
..*self
}
}
pub fn push_hyphen(&mut self, engine: &Engine, fallback: bool) {
self.insert_hyphen(engine, fallback, Side::Right)
}
pub fn prepend_hyphen(&mut self, engine: &Engine, fallback: bool) {
self.insert_hyphen(engine, fallback, Side::Left)
}
fn insert_hyphen(&mut self, engine: &Engine, fallback: bool, side: Side) {
let world = engine.world;
let book = world.book();
let fallback_func = if fallback {
Some(|| book.select_fallback(None, self.variant, "-"))
} else {
None
};
let mut chain = families(self.styles)
.map(|family| book.select(family, self.variant))
.chain(fallback_func.iter().map(|f| f()))
.flatten();
chain.find_map(|id| {
let font = world.font(id)?;
let ttf = font.ttf();
let glyph_id = ttf.glyph_index('-')?;
let x_advance = font.to_em(ttf.glyph_hor_advance(glyph_id)?);
let range = match side {
Side::Left => self.glyphs.first().map(|g| g.range.start..g.range.start),
Side::Right => self.glyphs.last().map(|g| g.range.end..g.range.end),
}
.unwrap_or_else(|| self.base..self.base);
self.width += x_advance.at(self.size);
let glyph = ShapedGlyph {
font,
glyph_id: glyph_id.0,
x_advance,
x_offset: Em::zero(),
y_offset: Em::zero(),
adjustability: Adjustability::default(),
range,
safe_to_break: true,
c: '-',
is_justifiable: false,
script: Script::Common,
};
match side {
Side::Left => self.glyphs.to_mut().insert(0, glyph),
Side::Right => self.glyphs.to_mut().push(glyph),
}
Some(())
});
}
fn slice_safe_to_break(&self, text_range: Range) -> Option<&[ShapedGlyph]> {
let Range { mut start, mut end } = text_range;
if !self.dir.is_positive() {
std::mem::swap(&mut start, &mut end);
}
let left = self.find_safe_to_break(start)?;
let right = self.find_safe_to_break(end)?;
Some(&self.glyphs[left..right])
}
fn find_safe_to_break(&self, text_index: usize) -> Option<usize> {
let ltr = self.dir.is_positive();
let len = self.glyphs.len();
if text_index == self.base {
return Some(if ltr { 0 } else { len });
} else if text_index == self.base + self.text.len() {
return Some(if ltr { len } else { 0 });
}
let found = self.glyphs.binary_search_by(|g: &ShapedGlyph| {
let ordering = g.range.start.cmp(&text_index);
if ltr {
ordering
} else {
ordering.reverse()
}
});
let mut idx = match found {
Ok(idx) => idx,
Err(idx) => {
return (idx > 0
&& self.glyphs[idx - 1].range.end == text_index
&& self.text[text_index - self.base..].starts_with('\n'))
.then_some(idx);
}
};
let dec = if ltr { usize::checked_sub } else { usize::checked_add };
while let Some(next) = dec(idx, 1) {
if self.glyphs.get(next).map_or(true, |g| g.range.start != text_index) {
break;
}
idx = next;
}
self.glyphs[idx].safe_to_break.then_some(idx + usize::from(!ltr))
}
}
impl Debug for ShapedText<'_> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.text.fmt(f)
}
}
pub fn shape_range<'a>(
items: &mut Vec<(Range, Item<'a>)>,
engine: &Engine,
text: &'a str,
bidi: &BidiInfo<'a>,
range: Range,
styles: StyleChain<'a>,
) {
let script = TextElem::script_in(styles);
let lang = TextElem::lang_in(styles);
let region = TextElem::region_in(styles);
let mut process = |range: Range, level: BidiLevel| {
let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL };
let shaped =
shape(engine, range.start, &text[range.clone()], styles, dir, lang, region);
items.push((range, Item::Text(shaped)));
};
let mut prev_level = BidiLevel::ltr();
let mut prev_script = Script::Unknown;
let mut cursor = range.start;
for i in range.clone() {
if !text.is_char_boundary(i) {
continue;
}
let level = bidi.levels[i];
let curr_script = match script {
Smart::Auto => {
text[i..].chars().next().map_or(Script::Unknown, |c| c.script())
}
Smart::Custom(_) => Script::Unknown,
};
if level != prev_level || !is_compatible(curr_script, prev_script) {
if cursor < i {
process(cursor..i, prev_level);
}
cursor = i;
prev_level = level;
prev_script = curr_script;
} else if is_generic_script(prev_script) {
prev_script = curr_script;
}
}
process(cursor..range.end, prev_level);
}
fn is_generic_script(script: Script) -> bool {
matches!(script, Script::Unknown | Script::Common | Script::Inherited)
}
fn is_compatible(a: Script, b: Script) -> bool {
is_generic_script(a) || is_generic_script(b) || a == b
}
#[allow(clippy::too_many_arguments)]
fn shape<'a>(
engine: &Engine,
base: usize,
text: &'a str,
styles: StyleChain<'a>,
dir: Dir,
lang: Lang,
region: Option<Region>,
) -> ShapedText<'a> {
let size = TextElem::size_in(styles);
let mut ctx = ShapingContext {
engine,
size,
glyphs: vec![],
used: vec![],
styles,
variant: variant(styles),
features: features(styles),
fallback: TextElem::fallback_in(styles),
dir,
};
if !text.is_empty() {
shape_segment(&mut ctx, base, text, families(styles));
}
track_and_space(&mut ctx);
calculate_adjustability(&mut ctx, lang, region);
#[cfg(debug_assertions)]
assert_all_glyphs_in_range(&ctx.glyphs, text, base..(base + text.len()));
#[cfg(debug_assertions)]
assert_glyph_ranges_in_order(&ctx.glyphs, dir);
ShapedText {
base,
text,
dir,
lang,
region,
styles,
variant: ctx.variant,
size,
width: ctx.glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(size),
glyphs: Cow::Owned(ctx.glyphs),
}
}
struct ShapingContext<'a, 'v> {
engine: &'a Engine<'v>,
glyphs: Vec<ShapedGlyph>,
used: Vec<Font>,
styles: StyleChain<'a>,
size: Abs,
variant: FontVariant,
features: Vec<rustybuzz::Feature>,
fallback: bool,
dir: Dir,
}
fn shape_segment<'a>(
ctx: &mut ShapingContext,
base: usize,
text: &str,
mut families: impl Iterator<Item = &'a str> + Clone,
) {
if text
.chars()
.all(|c| c == '\n' || c == '\t' || is_default_ignorable(c))
{
return;
}
let world = ctx.engine.world;
let book = world.book();
let mut selection = families.find_map(|family| {
book.select(family, ctx.variant)
.and_then(|id| world.font(id))
.filter(|font| !ctx.used.contains(font))
});
if selection.is_none() && ctx.fallback {
let first = ctx.used.first().map(Font::info);
selection = book
.select_fallback(first, ctx.variant, text)
.and_then(|id| world.font(id))
.filter(|font| !ctx.used.contains(font));
}
let Some(font) = selection else {
if let Some(font) = ctx.used.first().cloned() {
shape_tofus(ctx, base, text, font);
}
return;
};
ctx.used.push(font.clone());
let mut buffer = UnicodeBuffer::new();
buffer.push_str(text);
buffer.set_language(language(ctx.styles));
if let Some(script) = TextElem::script_in(ctx.styles).custom().and_then(|script| {
rustybuzz::Script::from_iso15924_tag(Tag::from_bytes(script.as_bytes()))
}) {
buffer.set_script(script)
}
buffer.set_direction(match ctx.dir {
Dir::LTR => rustybuzz::Direction::LeftToRight,
Dir::RTL => rustybuzz::Direction::RightToLeft,
_ => unimplemented!("vertical text layout"),
});
buffer.guess_segment_properties();
buffer.set_flags(BufferFlags::REMOVE_DEFAULT_IGNORABLES);
let plan = create_shape_plan(
&font,
buffer.direction(),
buffer.script(),
buffer.language().as_ref(),
&ctx.features,
);
let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer);
let infos = buffer.glyph_infos();
let pos = buffer.glyph_positions();
let ltr = ctx.dir.is_positive();
let mut i = 0;
while i < infos.len() {
let info = &infos[i];
let cluster = info.cluster as usize;
if info.glyph_id != 0 {
let start = base + cluster;
let end = base
+ if ltr { i.checked_add(1) } else { i.checked_sub(1) }
.and_then(|last| infos.get(last))
.map_or(text.len(), |info| info.cluster as usize);
let c = text[cluster..].chars().next().unwrap();
let script = c.script();
let x_advance = font.to_em(pos[i].x_advance);
ctx.glyphs.push(ShapedGlyph {
font: font.clone(),
glyph_id: info.glyph_id as u16,
x_advance,
x_offset: font.to_em(pos[i].x_offset),
y_offset: font.to_em(pos[i].y_offset),
adjustability: Adjustability::default(),
range: start..end,
safe_to_break: !info.unsafe_to_break(),
c,
is_justifiable: is_justifiable(
c,
script,
x_advance,
Adjustability::default().stretchability,
),
script,
});
} else {
let k = i;
while infos.get(i + 1).is_some_and(|info| info.glyph_id == 0) {
i += 1;
}
let start = infos[if ltr { k } else { i }].cluster as usize;
let end = if ltr { i.checked_add(1) } else { k.checked_sub(1) }
.and_then(|last| infos.get(last))
.map_or(text.len(), |info| info.cluster as usize);
let remove = base + start..base + end;
while ctx.glyphs.last().is_some_and(|g| remove.contains(&g.range.start)) {
ctx.glyphs.pop();
}
shape_segment(ctx, base + start, &text[start..end], families.clone());
}
i += 1;
}
ctx.used.pop();
}
#[comemo::memoize]
fn create_shape_plan(
font: &Font,
direction: rustybuzz::Direction,
script: rustybuzz::Script,
language: Option<&rustybuzz::Language>,
features: &[rustybuzz::Feature],
) -> Arc<ShapePlan> {
Arc::new(rustybuzz::ShapePlan::new(
font.rusty(),
direction,
Some(script),
language,
features,
))
}
fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) {
let x_advance = font.advance(0).unwrap_or_default();
let add_glyph = |(cluster, c): (usize, char)| {
let start = base + cluster;
let end = start + c.len_utf8();
let script = c.script();
ctx.glyphs.push(ShapedGlyph {
font: font.clone(),
glyph_id: 0,
x_advance,
x_offset: Em::zero(),
y_offset: Em::zero(),
adjustability: Adjustability::default(),
range: start..end,
safe_to_break: true,
c,
is_justifiable: is_justifiable(
c,
script,
x_advance,
Adjustability::default().stretchability,
),
script,
});
};
if ctx.dir.is_positive() {
text.char_indices().for_each(add_glyph);
} else {
text.char_indices().rev().for_each(add_glyph);
}
}
fn track_and_space(ctx: &mut ShapingContext) {
let tracking = Em::from_length(TextElem::tracking_in(ctx.styles), ctx.size);
let spacing =
TextElem::spacing_in(ctx.styles).map(|abs| Em::from_length(abs, ctx.size));
let mut glyphs = ctx.glyphs.iter_mut().peekable();
while let Some(glyph) = glyphs.next() {
if glyph.c == '\u{00A0}' {
glyph.x_advance -= nbsp_delta(&glyph.font).unwrap_or_default();
}
if glyph.is_space() {
glyph.x_advance = spacing.relative_to(glyph.x_advance);
}
if glyphs
.peek()
.is_some_and(|next| glyph.range.start != next.range.start)
{
glyph.x_advance += tracking;
}
}
}
fn calculate_adjustability(ctx: &mut ShapingContext, lang: Lang, region: Option<Region>) {
let style = cjk_punct_style(lang, region);
for glyph in &mut ctx.glyphs {
glyph.adjustability = glyph.base_adjustability(style);
}
let mut glyphs = ctx.glyphs.iter_mut().peekable();
while let Some(glyph) = glyphs.next() {
if glyph.is_cjk_punctuation() && matches!(style, CjkPunctStyle::Cns) {
continue;
}
let Some(next) = glyphs.peek_mut() else { continue };
let width = glyph.x_advance;
let delta = width / 2.0;
if glyph.is_cjk_punctuation()
&& next.is_cjk_punctuation()
&& (glyph.shrinkability().1 + next.shrinkability().0) >= delta
{
let left_delta = glyph.shrinkability().1.min(delta);
glyph.shrink_right(left_delta);
next.shrink_left(delta - left_delta);
}
}
}
fn nbsp_delta(font: &Font) -> Option<Em> {
let space = font.ttf().glyph_index(' ')?.0;
let nbsp = font.ttf().glyph_index('\u{00A0}')?.0;
Some(font.advance(nbsp)? - font.advance(space)?)
}
fn language(styles: StyleChain) -> rustybuzz::Language {
let mut bcp: EcoString = TextElem::lang_in(styles).as_str().into();
if let Some(region) = TextElem::region_in(styles) {
bcp.push('-');
bcp.push_str(region.as_str());
}
rustybuzz::Language::from_str(&bcp).unwrap()
}
#[cfg(debug_assertions)]
fn assert_all_glyphs_in_range(glyphs: &[ShapedGlyph], text: &str, range: Range) {
if glyphs
.iter()
.any(|g| g.range.start < range.start || g.range.end > range.end)
{
panic!("one or more glyphs in {text:?} fell out of range");
}
}
#[cfg(debug_assertions)]
fn assert_glyph_ranges_in_order(glyphs: &[ShapedGlyph], dir: Dir) {
if glyphs.is_empty() {
return;
}
for i in 0..(glyphs.len() - 1) {
let a = &glyphs[i];
let b = &glyphs[i + 1];
let ord = a.range.start.cmp(&b.range.start);
let ord = if dir.is_positive() { ord } else { ord.reverse() };
if ord == std::cmp::Ordering::Greater {
panic!(
"glyph ranges should be monotonically {}, \
but found glyphs out of order:\n\n\
first: {a:#?}\nsecond: {b:#?}",
if dir.is_positive() { "increasing" } else { "decreasing" },
);
}
}
}
pub const BEGIN_PUNCT_PAT: &[char] =
&['“', '‘', '《', '〈', '(', '『', '「', '【', '〖', '〔', '[', '{'];
pub const END_PUNCT_PAT: &[char] = &[
'”', '’', ',', '.', '。', '、', ':', ';', '》', '〉', ')', '』', '」', '】',
'〗', '〕', ']', '}', '?', '!',
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CjkPunctStyle {
Gb,
Cns,
Jis,
}
pub fn cjk_punct_style(lang: Lang, region: Option<Region>) -> CjkPunctStyle {
match (lang, region.as_ref().map(Region::as_str)) {
(Lang::CHINESE, Some("TW" | "HK")) => CjkPunctStyle::Cns,
(Lang::JAPANESE, _) => CjkPunctStyle::Jis,
_ => CjkPunctStyle::Gb,
}
}
fn is_space(c: char) -> bool {
matches!(c, ' ' | '\u{00A0}' | ' ')
}
pub fn is_of_cj_script(c: char) -> bool {
is_cj_script(c, c.script())
}
fn is_cj_script(c: char, script: Script) -> bool {
use Script::*;
matches!(script, Hiragana | Katakana | Han) || c == '\u{30FC}'
}
fn is_cjk_left_aligned_punctuation(
c: char,
x_advance: Em,
stretchability: (Em, Em),
style: CjkPunctStyle,
) -> bool {
use CjkPunctStyle::*;
if matches!(c, '”' | '’') && x_advance + stretchability.1 == Em::one() {
return true;
}
if matches!(style, Gb | Jis) && matches!(c, ',' | '。' | '.' | '、' | ':' | ';')
{
return true;
}
if matches!(style, Gb) && matches!(c, '?' | '!') {
return true;
}
matches!(c, '》' | ')' | '』' | '」' | '】' | '〗' | '〕' | '〉' | ']' | '}')
}
fn is_cjk_right_aligned_punctuation(
c: char,
x_advance: Em,
stretchability: (Em, Em),
) -> bool {
if matches!(c, '“' | '‘') && x_advance + stretchability.0 == Em::one() {
return true;
}
matches!(c, '《' | '(' | '『' | '「' | '【' | '〖' | '〔' | '〈' | '[' | '{')
}
fn is_cjk_center_aligned_punctuation(c: char, style: CjkPunctStyle) -> bool {
if matches!(style, CjkPunctStyle::Cns)
&& matches!(c, ',' | '。' | '.' | '、' | ':' | ';')
{
return true;
}
matches!(c, '\u{30FB}' | '\u{00B7}')
}
fn is_justifiable(
c: char,
script: Script,
x_advance: Em,
stretchability: (Em, Em),
) -> bool {
let style = CjkPunctStyle::Gb;
is_space(c)
|| is_cj_script(c, script)
|| is_cjk_left_aligned_punctuation(c, x_advance, stretchability, style)
|| is_cjk_right_aligned_punctuation(c, x_advance, stretchability)
|| is_cjk_center_aligned_punctuation(c, style)
}