use std::{
cmp::Ordering,
ops::{Range, RangeInclusive},
sync::LazyLock,
};
use icu_properties::{
CodePointMapData, CodePointSetData, CodePointSetDataBorrowed,
props::{DefaultIgnorableCodePoint, EmojiPresentation, GraphemeClusterBreak},
};
use smallvec::SmallVec;
use crate::{coverage::Coverage, typeface::Typeface};
pub(crate) struct FallbackChain<'a> {
links: SmallVec<[Link<'a>; 8]>,
}
struct Link<'a> {
typeface: usize,
coverage: &'a Coverage,
is_color: bool,
}
impl<'a> FallbackChain<'a> {
pub(crate) fn new(typefaces: impl Iterator<Item = (usize, &'a Typeface)>) -> Self {
Self {
links: typefaces
.map(|(typeface, face)| Link {
typeface,
coverage: face.coverage(),
is_color: face.is_color(),
})
.collect(),
}
}
pub(crate) fn split(&self, text: &str, mut visit: impl FnMut(Range<usize>, usize)) {
let mut split = Split {
chain: self,
run: None,
cluster_start: 0,
boundaries: ClusterBoundaries {
previous: GraphemeClusterBreak::Control,
flag_open: false,
},
};
let mut chars = text.char_indices().peekable();
while let Some((at, ch)) = chars.next() {
let joins = split.boundaries.joins(ch);
match split.run {
Some(run) if joins => split.extend_cluster(text, at, ch, run, &mut visit),
run => {
let next = chars.peek().map(|&(_, next)| next);
split.start_cluster(at, ch, next, run, &mut visit);
}
}
}
if let Some(run) = split.run {
visit(run.start..text.len(), self.links[run.link].typeface);
}
}
fn pick(&self, needed: &[char], wants_color: bool) -> Option<usize> {
let mut covering = None;
for (position, link) in self.links.iter().enumerate() {
if needed.iter().all(|&ch| link.coverage.contains(ch)) {
if link.is_color == wants_color {
return Some(position);
}
covering.get_or_insert(position);
}
}
covering
}
}
struct Split<'c, 'a> {
chain: &'c FallbackChain<'a>,
run: Option<Run>,
cluster_start: usize,
boundaries: ClusterBoundaries,
}
#[derive(Clone, Copy)]
struct Run {
start: usize,
link: usize,
}
impl Split<'_, '_> {
fn extend_cluster(
&mut self,
text: &str,
at: usize,
ch: char,
run: Run,
visit: &mut impl FnMut(Range<usize>, usize),
) {
let links = &self.chain.links;
if links[run.link].coverage.contains(ch) || needs_no_glyph(ch) {
return;
}
let Some(cluster) = text.get(self.cluster_start..at + ch.len_utf8()) else {
return;
};
let needed: SmallVec<[char; 8]> =
cluster.chars().filter(|&ch| !needs_no_glyph(ch)).collect();
if let Some(moved) = self.chain.pick(&needed, wants_color(cluster))
&& moved != run.link
{
if run.start < self.cluster_start {
visit(run.start..self.cluster_start, links[run.link].typeface);
}
self.run = Some(Run {
start: self.cluster_start,
link: moved,
});
}
}
fn start_cluster(
&mut self,
at: usize,
ch: char,
next: Option<char>,
run: Option<Run>,
visit: &mut impl FnMut(Range<usize>, usize),
) {
self.cluster_start = at;
let ignorable = needs_no_glyph(ch);
if let Some(run) = run
&& (ignorable || (sticky(ch) && self.chain.links[run.link].coverage.contains(ch)))
{
return;
}
let wants_color = match next {
Some('\u{FE0F}' | '\u{20E3}') => true,
Some('\u{FE0E}') => false,
_ => asks_presentation(ch),
};
let needed = [ch];
let link = self
.chain
.pick(if ignorable { &[] } else { &needed }, wants_color)
.or_else(|| run.map(|run| run.link))
.unwrap_or(0);
if run.is_none_or(|run| run.link != link) {
if let Some(run) = run {
visit(run.start..at, self.chain.links[run.link].typeface);
}
self.run = Some(Run { start: at, link });
}
}
}
struct ClusterBoundaries {
previous: GraphemeClusterBreak,
flag_open: bool,
}
impl ClusterBoundaries {
fn joins(&mut self, ch: char) -> bool {
use GraphemeClusterBreak as Class;
let class = CodePointMapData::<GraphemeClusterBreak>::new().get(ch);
let previous = self.previous;
let joins = matches!(
class,
Class::Extend | Class::SpacingMark | Class::ZWJ | Class::EModifier
) || matches!(previous, Class::Prepend | Class::ZWJ)
|| (previous == Class::L
&& matches!(class, Class::L | Class::V | Class::LV | Class::LVT))
|| (matches!(previous, Class::LV | Class::V) && matches!(class, Class::V | Class::T))
|| (matches!(previous, Class::LVT | Class::T) && class == Class::T)
|| (self.flag_open && class == Class::RegionalIndicator);
self.flag_open = class == Class::RegionalIndicator && !joins;
self.previous = class;
joins
}
}
pub(crate) fn draws_plainly(coverage: &Coverage, text: &str) -> bool {
let ascii = coverage.ascii() | ASCII_CONTROLS;
if ascii == u128::MAX && text.is_ascii() {
return true;
}
text.chars().all(|ch| {
if ch.is_ascii() {
ascii >> u32::from(ch) & 1 != 0
} else {
(coverage.contains(ch) || needs_no_glyph(ch)) && !asks_presentation(ch)
}
})
}
const ASCII_CONTROLS: u128 = ((1 << 0x20) - 1) | (1 << 0x7F);
fn sticky(ch: char) -> bool {
matches!(
ch,
' ' | '!' | ',' | '-' | '.' | ':' | ';' | '?' | '\u{00A0}' | '\u{1680}' | '\u{2000}'
..='\u{200A}' | '\u{2010}' | '\u{2011}' | '\u{202F}' | '\u{205F}' | '\u{3000}'
)
}
pub(crate) fn needs_no_glyph(ch: char) -> bool {
static IGNORABLE: LazyLock<CodePointSet> =
LazyLock::new(|| CodePointSet::of(CodePointSetData::new::<DefaultIgnorableCodePoint>()));
ch.is_control() || IGNORABLE.contains(ch)
}
fn asks_presentation(ch: char) -> bool {
static EMOJI_PRESENTATION: LazyLock<CodePointSet> =
LazyLock::new(|| CodePointSet::of(CodePointSetData::new::<EmojiPresentation>()));
ch >= '\u{20E3}'
&& (matches!(ch, '\u{20E3}' | '\u{FE0E}' | '\u{FE0F}') || EMOJI_PRESENTATION.contains(ch))
}
fn wants_color(cluster: &str) -> bool {
let mut wants = false;
for (position, ch) in cluster.chars().enumerate() {
match ch {
'\u{FE0E}' => return false,
'\u{FE0F}' | '\u{20E3}' => wants = true,
_ if position == 0 => wants = asks_presentation(ch),
_ => {}
}
}
wants
}
struct CodePointSet {
basic: Coverage,
astral: Box<[RangeInclusive<u32>]>,
}
impl CodePointSet {
fn of(set: CodePointSetDataBorrowed<'static>) -> Self {
const PLANE_ONE: u32 = 0x1_0000;
Self {
basic: Coverage::from_codepoints(
set.iter_ranges()
.flat_map(|range| *range.start()..=(*range.end()).min(PLANE_ONE - 1)),
),
astral: set
.iter_ranges()
.filter(|range| *range.end() >= PLANE_ONE)
.map(|range| (*range.start()).max(PLANE_ONE)..=*range.end())
.collect(),
}
}
fn contains(&self, ch: char) -> bool {
if ch <= '\u{FFFF}' {
return self.basic.contains(ch);
}
let codepoint = u32::from(ch);
self.astral
.binary_search_by(|range| {
if *range.end() < codepoint {
Ordering::Less
} else if *range.start() > codepoint {
Ordering::Greater
} else {
Ordering::Equal
}
})
.is_ok()
}
}