use std::borrow::Cow;
use unicode_segmentation::UnicodeSegmentation;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TabPolicy {
Fixed(u16),
SingleCell,
}
pub fn cell_width_char(ch: char) -> u16 {
if ch.is_control() {
return 0;
}
let u = ch as u32;
if matches!(
u,
0x0300..=0x036F | 0x1AB0..=0x1AFF | 0x1DC0..=0x1DFF | 0x20D0..=0x20FF | 0xFE20..=0xFE2F ) {
return 0;
}
if matches!(
u,
0x1100..=0x115F | 0x2329..=0x232A
| 0x2E80..=0xA4CF | 0xAC00..=0xD7A3 | 0xF900..=0xFAFF | 0xFE10..=0xFE19 | 0xFE30..=0xFE6F | 0xFF00..=0xFF60 | 0xFFE0..=0xFFE6
) {
return 2;
}
1
}
pub fn cell_width(s: &str, tab_policy: TabPolicy) -> u16 {
let mut width: u16 = 0;
for ch in s.chars() {
match ch {
'\t' => match tab_policy {
TabPolicy::Fixed(n) => width = width.saturating_add(n),
TabPolicy::SingleCell => width = width.saturating_add(1),
},
'\n' | '\r' => {
}
_ => width = width.saturating_add(cell_width_char(ch)),
}
}
width
}
pub fn clip_to_cells(s: &str, max_cells: u16, tab_policy: TabPolicy) -> String {
clip_to_cells_cow(s, max_cells, tab_policy).into_owned()
}
pub fn clip_to_cells_cow(s: &str, max_cells: u16, tab_policy: TabPolicy) -> Cow<'_, str> {
if max_cells == 0 {
return Cow::Borrowed(&s[..0]);
}
let mut out: Option<String> = None;
let mut used: u16 = 0;
for (byte_idx, ch) in s.char_indices() {
if ch == '\n' || ch == '\r' {
return match out {
Some(out) => Cow::Owned(out),
None => Cow::Borrowed(&s[..byte_idx]),
};
}
if ch == '\t' {
let tab_w = match tab_policy {
TabPolicy::Fixed(n) => n,
TabPolicy::SingleCell => 1,
};
if used.saturating_add(tab_w) > max_cells {
return match out {
Some(out) => Cow::Owned(out),
None => Cow::Borrowed(&s[..byte_idx]),
};
}
let spaces = tab_w as usize;
out.get_or_insert_with(|| s[..byte_idx].to_string())
.extend(std::iter::repeat(' ').take(spaces));
used = used.saturating_add(tab_w);
continue;
}
let w = cell_width_char(ch);
if w == 0 {
out.get_or_insert_with(|| s[..byte_idx].to_string());
continue;
}
if used.saturating_add(w) > max_cells {
return match out {
Some(out) => Cow::Owned(out),
None => Cow::Borrowed(&s[..byte_idx]),
};
}
if let Some(out) = &mut out {
out.push(ch);
}
used = used.saturating_add(w);
}
match out {
Some(out) => Cow::Owned(out),
None => Cow::Borrowed(s),
}
}
pub fn clip_to_cells_into(out: &mut String, s: &str, max_cells: u16, tab_policy: TabPolicy) {
out.clear();
out.push_str(&clip_to_cells_cow(s, max_cells, tab_policy));
}
pub fn clip_to_cells_ellipsis(s: &str, max_cells: u16, tab_policy: TabPolicy) -> String {
if max_cells == 0 {
return String::new();
}
if cell_width(s, tab_policy) <= max_cells {
return s.to_string();
}
let ell = '…';
let ell_w = cell_width_char(ell);
if ell_w > 0 && ell_w < max_cells {
let mut clipped = clip_to_cells(s, max_cells.saturating_sub(ell_w), tab_policy);
clipped.push(ell);
return clipped;
}
if max_cells >= 1 {
return ".".to_string();
}
String::new()
}
pub fn fit_to_cells(s: &str, target_cells: u16, tab_policy: TabPolicy, ellipsis: bool) -> String {
if target_cells == 0 {
return String::new();
}
let mut out = if ellipsis {
clip_to_cells_ellipsis(s, target_cells, tab_policy)
} else {
clip_to_cells(s, target_cells, tab_policy)
};
let used = cell_width(&out, TabPolicy::SingleCell);
if used < target_cells {
out.extend(std::iter::repeat(' ').take((target_cells - used) as usize));
}
out
}
pub fn byte_index_for_char_index(s: &str, char_idx: usize) -> usize {
if char_idx == 0 {
return 0;
}
s.char_indices()
.nth(char_idx)
.map(|(i, _)| i)
.unwrap_or(s.len())
}
pub fn cell_column_for_char_index(s: &str, char_idx: usize) -> u16 {
let mut col: u16 = 0;
for (i, ch) in s.chars().enumerate() {
if i >= char_idx {
break;
}
col = col.saturating_add(cell_width_char(ch));
}
col
}
pub fn char_index_from_cell_column(s: &str, col: u16) -> usize {
let mut acc: u16 = 0;
for (i, ch) in s.chars().enumerate() {
let w = cell_width_char(ch);
if w == 0 {
continue;
}
if acc.saturating_add(w) > col {
return i;
}
acc = acc.saturating_add(w);
}
s.chars().count()
}
pub fn grapheme_count(s: &str) -> usize {
UnicodeSegmentation::graphemes(s, true).count()
}
pub fn byte_index_for_grapheme_index(s: &str, grapheme_idx: usize) -> usize {
if grapheme_idx == 0 {
return 0;
}
UnicodeSegmentation::grapheme_indices(s, true)
.nth(grapheme_idx)
.map(|(i, _)| i)
.unwrap_or(s.len())
}
pub fn cell_column_for_grapheme_index(s: &str, grapheme_idx: usize, tab_policy: TabPolicy) -> u16 {
let mut col: u16 = 0;
for (i, g) in UnicodeSegmentation::graphemes(s, true).enumerate() {
if i >= grapheme_idx {
break;
}
col = col.saturating_add(cell_width(g, tab_policy));
}
col
}
pub fn grapheme_index_from_cell_column(s: &str, col: u16, tab_policy: TabPolicy) -> usize {
let mut acc: u16 = 0;
for (i, g) in UnicodeSegmentation::graphemes(s, true).enumerate() {
let w = cell_width(g, tab_policy);
if w == 0 {
continue;
}
if acc.saturating_add(w) > col {
return i;
}
acc = acc.saturating_add(w);
}
grapheme_count(s)
}