use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::{enums::Wrap, prelude::Vec2};
#[derive(Debug, Clone)]
pub struct TextParser<'a> {
text: Option<&'a str>,
wrap: Wrap,
}
impl<'a> TextParser<'a> {
pub fn new(text: &'a str) -> Self {
Self {
text: if text.is_empty() { None } else { Some(text) },
wrap: Wrap::default(),
}
}
pub fn wrap(mut self, wrap: Wrap) -> Self {
self.wrap = wrap;
self
}
pub fn next_line(&mut self, max_width: usize) -> Option<(&'a str, usize)> {
self.text?;
match self.wrap {
Wrap::Letter => self.lw_next_line(max_width),
Wrap::Word => self.ww_next_line(max_width),
}
}
pub fn width(&mut self, size: &Vec2) -> usize {
if size.x == 0 || size.y == 0 {
return 0;
}
let width = self.text.unwrap_or_default().width();
if width == 0 {
return 0;
}
let mut low = width.div_ceil(size.y).max(1);
let mut high = width;
while low < high {
let mid = low + (high - low) / 2;
let mut parser = self.clone();
let height = parser.inner_height(mid);
if height <= size.y {
high = mid;
} else {
low = mid + 1
}
}
low
}
pub fn height(&mut self, size: &Vec2) -> usize {
if size.x == 0 || size.y == 0 {
return 0;
}
self.inner_height(size.x)
}
pub fn ww_next_line(
&mut self,
max_width: usize,
) -> Option<(&'a str, usize)> {
let text = self.text.unwrap_or_default();
let mut last = None;
let mut was_whitespace = true;
let mut width = 0;
for (id, grapheme) in text.grapheme_indices(true) {
let is_whitespace = grapheme.starts_with(char::is_whitespace);
if is_whitespace && !was_whitespace {
last = Some((id, width));
}
if grapheme == "\n" {
let (idx, w) = last.unwrap_or({
if was_whitespace { (0, 0) } else { (id, width) }
});
let line = &text[..idx];
self.text = Some(&text[id + 1..]);
return Some((line, w));
}
let grapheme_width = grapheme.width();
if width + grapheme_width > max_width {
let (idx, w) = last.unwrap_or((id, width));
let line = &text[..idx];
let rest = text[idx..].trim_start();
self.text = if rest.is_empty() { None } else { Some(rest) };
return Some((line, w));
}
width += grapheme_width;
was_whitespace = is_whitespace;
}
let (idx, w) = if was_whitespace {
last.unwrap_or((0, 0))
} else {
(text.len(), width)
};
let line = &text[..idx];
self.text = None;
Some((line, w))
}
pub fn lw_next_line(
&mut self,
max_width: usize,
) -> Option<(&'a str, usize)> {
let text = self.text.unwrap_or_default();
let mut width = 0;
for (id, grapheme) in text.grapheme_indices(true) {
if grapheme == "\n" {
let line = &text[..id];
self.text = Some(&text[id + 1..]);
return Some((line, width));
}
let grapheme_width = grapheme.width();
if width + grapheme_width > max_width {
let line = &text[..id];
self.text = Some(&text[id..]);
return Some((line, width));
}
width += grapheme_width;
}
self.text = None;
Some((text, width))
}
pub fn is_end(&self) -> bool {
self.text.is_none()
}
}
impl<'a> TextParser<'a> {
fn inner_height(&mut self, width: usize) -> usize {
let mut height = 0;
while self.next_line(width).is_some() {
height += 1;
}
height
}
}
#[cfg(test)]
mod tests {
use crate::enums::Wrap;
use super::TextParser;
#[test]
fn wrap() {
let parser = TextParser::new("test").wrap(Wrap::Letter);
assert_eq!(parser.wrap, Wrap::Letter);
}
#[test]
fn is_end() {
let mut parser = TextParser::new("end test ");
assert_eq!(parser.next_line(5), Some(("end".into(), 3)));
assert!(!parser.is_end());
assert_eq!(parser.next_line(5), Some(("test".into(), 4)));
assert!(parser.is_end());
}
#[test]
fn is_end_empty() {
let parser = TextParser::new("");
assert!(parser.is_end());
}
#[test]
fn next_line() {
let text = "This is \t a test of něxt line ";
let mut parser = TextParser::new(text);
assert_eq!(parser.next_line(15), Some(("This is".into(), 11)));
assert_eq!(parser.next_line(7), Some(("a test".into(), 6)));
assert_eq!(parser.next_line(15), Some(("of něxt line".into(), 13)));
assert_eq!(parser.next_line(15), None);
assert_eq!(parser.next_line(15), None);
assert!(parser.is_end());
}
#[test]
fn next_line_newline() {
let text = " This is \n a \n \n test ";
let mut parser = TextParser::new(text);
assert_eq!(parser.next_line(14), Some((" This is".into(), 10)));
assert_eq!(parser.next_line(14), Some((" a".into(), 2)));
assert_eq!(parser.next_line(14), Some(("".into(), 0)));
assert_eq!(parser.next_line(14), Some((" test".into(), 6)));
assert_eq!(parser.next_line(14), None);
assert!(parser.is_end());
}
#[test]
fn next_line_unicode_width() {
let text = "a 字 🚀 b";
let mut parser = TextParser::new(text);
assert_eq!(parser.next_line(4), Some(("a 字", 4)));
assert_eq!(parser.next_line(4), Some(("🚀 b", 4)));
assert!(parser.is_end());
}
#[test]
fn next_line_word_too_long() {
let text = "Hello termint";
let mut parser = TextParser::new(text);
assert_eq!(parser.next_line(6), Some(("Hello", 5)));
assert_eq!(parser.next_line(5), Some(("termi", 5)));
assert_eq!(parser.next_line(5), Some(("nt", 2)));
}
#[test]
fn next_line_trailing_newline() {
let text = "Line\n";
let mut parser = TextParser::new(text);
assert_eq!(parser.next_line(10), Some(("Line", 4)));
assert_eq!(parser.next_line(10), Some(("", 0)));
assert_eq!(parser.next_line(10), None);
}
#[test]
fn next_line_letter_wrap() {
let text = "This is a test of něxt line";
let mut parser = TextParser::new(text).wrap(Wrap::Letter);
assert_eq!(parser.next_line(15), Some(("This is a tes".into(), 15)));
assert_eq!(parser.next_line(5), Some(("t of".into(), 5)));
assert_eq!(parser.next_line(15), Some((" něxt line".into(), 11)));
assert_eq!(parser.next_line(15), None);
assert_eq!(parser.next_line(15), None);
assert!(parser.is_end());
}
#[test]
fn next_line_letter_wrap_newline() {
let text = " This is \n a \n\n test ";
let mut parser = TextParser::new(text).wrap(Wrap::Letter);
assert_eq!(parser.next_line(14), Some((" This is ".into(), 12)));
assert_eq!(parser.next_line(14), Some((" a ".into(), 3)));
assert_eq!(parser.next_line(14), Some(("".into(), 0)));
assert_eq!(parser.next_line(14), Some((" test ".into(), 7)));
assert_eq!(parser.next_line(14), None);
assert!(parser.is_end());
}
}