use crate::{
encoding::Encoding,
grapheme::{ClusterState, width_value},
props::{CB_LV, CB_LVT, CB_MASK, CB_OTHER, EPIC_BIT, WIDTH_EMOJI_TEXT, WIDTH_SHIFT, props},
simd::plain_prefix,
unit::Unit,
utf8::Utf8,
};
#[inline(always)]
const fn simple(p: u8) -> bool {
matches!(p & CB_MASK, CB_OTHER | CB_LV | CB_LVT)
&& p & EPIC_BIT == 0
&& (p >> WIDTH_SHIFT) & 3 != WIDTH_EMOJI_TEXT
}
#[inline(always)]
const fn promotable_ascii(cp: u32) -> bool {
matches!(cp, 0x23 | 0x2a | 0x30..=0x39)
}
pub fn width<E: Encoding>(input: &[E::Unit]) -> usize {
measure_width::<E, false>(input, 0).expect("unbounded width scan cannot fail")
}
#[inline(always)]
fn commit_width<const BOUNDED: bool>(measured: &mut usize, width: usize, max_width: usize) -> bool {
if BOUNDED && width > max_width - *measured {
false
} else {
*measured += width;
true
}
}
fn measure_width<E: Encoding, const BOUNDED: bool>(
input: &[E::Unit],
max_width: usize,
) -> Option<usize> {
let mut measured = 0usize;
let mut rest = input;
'bulk: loop {
if !E::FOREIGN {
let window = if BOUNDED {
let budget = max_width - measured;
rest.len().min(budget.saturating_add(1))
} else {
rest.len()
};
let run = plain_prefix(&rest[..window]);
if run > 0 {
let promote = promotable_ascii(rest[run - 1].to_u32());
let take = if run < rest.len() && promote {
run - 1
} else {
run
};
if !commit_width::<BOUNDED>(&mut measured, take, max_width) {
return None;
}
rest = &rest[take..];
}
}
if rest.is_empty() {
return Some(measured);
}
let mut cp = E::decode(&mut rest);
let mut p = props(cp);
'cluster: loop {
let mut state = 'simple: {
while simple(p) {
if rest.is_empty() {
return commit_width::<BOUNDED>(&mut measured, width_value(p), max_width)
.then_some(measured);
}
let mut peek = rest;
let next = E::decode(&mut peek);
if !E::FOREIGN && next.wrapping_sub(0x20) <= 0x5e {
if !commit_width::<BOUNDED>(&mut measured, width_value(p), max_width) {
return None;
}
continue 'bulk;
}
let np = props(next);
if !simple(np) {
let mut state = ClusterState::start(cp, p);
rest = peek;
if state.try_join(next, np) {
break 'simple state;
}
if !commit_width::<BOUNDED>(&mut measured, state.finish(), max_width) {
return None;
}
break 'simple ClusterState::start(next, np);
}
if !commit_width::<BOUNDED>(&mut measured, width_value(p), max_width) {
return None;
}
cp = next;
p = np;
rest = peek;
}
break 'simple ClusterState::start(cp, p);
};
loop {
if rest.is_empty() {
return commit_width::<BOUNDED>(&mut measured, state.finish(), max_width)
.then_some(measured);
}
let mut peek = rest;
let next = E::decode(&mut peek);
if !E::FOREIGN && next.wrapping_sub(0x20) <= 0x5e && !state.joins_plain() {
if !commit_width::<BOUNDED>(&mut measured, state.finish(), max_width) {
return None;
}
continue 'bulk;
}
let np = props(next);
if state.try_join(next, np) {
rest = peek;
continue;
}
if !commit_width::<BOUNDED>(&mut measured, state.finish(), max_width) {
return None;
}
rest = peek;
if simple(np) {
cp = next;
p = np;
continue 'cluster;
}
state = ClusterState::start(next, np);
}
}
}
}
#[inline]
pub fn width_str(input: &str) -> usize {
width::<Utf8>(input.as_bytes())
}
pub fn width_within<E: Encoding>(input: &[E::Unit], max_width: usize) -> Option<usize> {
measure_width::<E, true>(input, max_width)
}
#[inline]
pub fn width_within_str(input: &str, max_width: usize) -> Option<usize> {
width_within::<Utf8>(input.as_bytes(), max_width)
}
#[inline]
pub fn width_char(c: char) -> usize {
width_value(props(c as u32))
}