use crate::error::{DocError, DocResult};
pub use super::vt_font_map::VtFontProvider;
pub use super::vt_line::Line;
pub use super::vt_section::Section;
pub use super::vt_word_info::WordInfo;
pub use super::vt_word_place::WordPlace;
pub use super::vt_word_range::WordRange;
pub type VtIterator<'a, P> = VtWordIterator<'a, P>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Alignment {
#[default]
Left,
Center,
Right,
}
impl Alignment {
pub fn from_value(v: i32) -> Self {
match v {
1 => Self::Center,
2 => Self::Right,
_ => Self::Left,
}
}
}
const AUTO_FONT_SIZES: &[f32] = &[
4.0, 6.0, 8.0, 9.0, 10.0, 11.0, 12.0, 14.0, 16.0, 18.0, 20.0, 22.0, 24.0, 26.0, 28.0, 36.0,
48.0, 72.0, 144.0,
];
pub struct VariableText<P: VtFontProvider> {
provider: P,
sections: Vec<Section>,
plate_rect: [f32; 4],
font_size: f32,
alignment: Alignment,
multi_line: bool,
auto_return: bool,
auto_font_size: bool,
char_array: Option<usize>,
password_char: Option<char>,
limit_char: Option<usize>,
content_rect: [f32; 4],
}
impl<P: VtFontProvider> VariableText<P> {
pub fn new(provider: P) -> Self {
Self {
provider,
sections: Vec::new(),
plate_rect: [0.0; 4],
font_size: 12.0,
alignment: Alignment::Left,
multi_line: false,
auto_return: true,
auto_font_size: false,
char_array: None,
password_char: None,
limit_char: None,
content_rect: [0.0; 4],
}
}
pub fn set_plate_rect(&mut self, rect: [f32; 4]) {
self.plate_rect = rect;
}
pub fn plate_rect(&self) -> [f32; 4] {
self.plate_rect
}
#[inline]
pub fn get_plate_rect(&self) -> [f32; 4] {
self.plate_rect()
}
pub fn set_font_size(&mut self, size: f32) {
self.font_size = size;
}
pub fn set_alignment(&mut self, alignment: Alignment) {
self.alignment = alignment;
}
pub fn set_multi_line(&mut self, multi_line: bool) {
self.multi_line = multi_line;
}
pub fn set_auto_return(&mut self, auto_return: bool) {
self.auto_return = auto_return;
}
pub fn set_auto_font_size(&mut self, auto: bool) {
self.auto_font_size = auto;
}
pub fn set_char_array(&mut self, count: Option<usize>) {
self.char_array = count;
}
pub fn set_password_char(&mut self, ch: Option<char>) {
self.password_char = ch;
}
pub fn set_limit_char(&mut self, limit: Option<usize>) {
self.limit_char = limit;
}
pub fn set_text(&mut self, text: &str) {
self.sections.clear();
let effective_text: String = if let Some(limit) = self.limit_char {
text.chars().take(limit).collect()
} else {
text.to_string()
};
let display_text = if let Some(pw) = self.password_char {
pw.to_string().repeat(effective_text.chars().count())
} else {
effective_text
};
let mut current_section = Section {
words: Vec::new(),
lines: Vec::new(),
rect: [0.0; 4],
};
let mut chars = display_text.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\r' {
if chars.peek() == Some(&'\n') {
chars.next(); }
self.sections.push(current_section);
current_section = Section {
words: Vec::new(),
lines: Vec::new(),
rect: [0.0; 4],
};
} else if ch == '\n' {
self.sections.push(current_section);
current_section = Section {
words: Vec::new(),
lines: Vec::new(),
rect: [0.0; 4],
};
} else {
current_section.words.push(WordInfo {
character: ch,
font_index: 0,
position_x: 0.0,
width: 0.0,
});
}
}
self.sections.push(current_section);
}
pub fn rearrange(&mut self) {
if self.auto_font_size {
self.font_size = self.find_auto_font_size();
}
let plate_width = self.plate_rect[2] - self.plate_rect[0];
let mut total_y = self.plate_rect[3];
for section in &mut self.sections {
for word in &mut section.words {
word.width = self.provider.char_width(
word.font_index,
word.character as u16,
self.font_size,
);
}
section.lines.clear();
if let Some(cell_count) = self.char_array {
let cell_width = if cell_count > 0 {
plate_width / cell_count as f32
} else {
plate_width
};
let ascent = self.provider.ascent(0, self.font_size);
let descent = self.provider.descent(0, self.font_size);
let mut x_pos = 0.0;
for (i, word) in section.words.iter_mut().enumerate() {
if i >= cell_count {
break;
}
word.position_x = x_pos + (cell_width - word.width) / 2.0;
x_pos += cell_width;
}
let line_width = section
.words
.iter()
.take(cell_count)
.next_back()
.map(|w| w.position_x + w.width)
.unwrap_or(0.0);
let word_count = section.words.len().min(cell_count);
total_y -= ascent;
section.lines.push(Line {
word_start: 0,
word_count,
x: 0.0,
y: total_y,
width: line_width,
ascent,
descent,
});
total_y += descent;
} else {
let ascent = self.provider.ascent(0, self.font_size);
let descent = self.provider.descent(0, self.font_size);
let mut line_start = 0;
let mut line_width = 0.0_f32;
let mut x_pos = 0.0_f32;
for (i, word) in section.words.iter_mut().enumerate() {
let char_width = word.width;
let should_break = self.multi_line
&& self.auto_return
&& line_width + char_width > plate_width
&& i > line_start;
if should_break {
let word_count = i - line_start;
total_y -= ascent;
section.lines.push(Line {
word_start: line_start,
word_count,
x: 0.0,
y: total_y,
width: line_width,
ascent,
descent,
});
total_y += descent;
line_start = i;
line_width = 0.0;
x_pos = 0.0;
}
word.position_x = x_pos;
x_pos += char_width;
line_width += char_width;
}
let word_count = section.words.len() - line_start;
if word_count > 0 || section.lines.is_empty() {
total_y -= ascent;
section.lines.push(Line {
word_start: line_start,
word_count,
x: 0.0,
y: total_y,
width: line_width,
ascent,
descent,
});
total_y += descent;
}
}
for line in &mut section.lines {
line.x = match self.alignment {
Alignment::Left => 0.0,
Alignment::Center => (plate_width - line.width) / 2.0,
Alignment::Right => plate_width - line.width,
};
}
if let (Some(first), Some(last)) = (section.lines.first(), section.lines.last()) {
section.rect = [
self.plate_rect[0],
last.y + last.descent,
self.plate_rect[2],
first.y + first.ascent,
];
}
}
if let (Some(first_sec), Some(last_sec)) = (self.sections.first(), self.sections.last()) {
self.content_rect = [
self.plate_rect[0],
last_sec.rect[1],
self.plate_rect[2],
first_sec.rect[3],
];
}
}
pub fn content_rect(&self) -> [f32; 4] {
self.content_rect
}
#[inline]
pub fn get_content_rect(&self) -> [f32; 4] {
self.content_rect()
}
pub fn sections(&self) -> &[Section] {
&self.sections
}
pub fn font_size(&self) -> f32 {
self.font_size
}
pub fn hit_test(&self, x: f32, y: f32) -> Option<WordPlace> {
let rel_x = x - self.plate_rect[0];
let rel_y = y;
for (si, section) in self.sections.iter().enumerate() {
for (li, line) in section.lines.iter().enumerate() {
let line_top = line.y + line.ascent;
let line_bottom = line.y + line.descent;
if rel_y <= line_top && rel_y >= line_bottom {
let line_x = rel_x - line.x;
let end = line.word_start + line.word_count;
for wi in line.word_start..end {
let word = §ion.words[wi];
if line_x <= word.position_x + word.width / 2.0 {
return Some(WordPlace {
section: si,
line: li,
word: wi - line.word_start,
});
}
}
return Some(WordPlace {
section: si,
line: li,
word: line.word_count.saturating_sub(1),
});
}
}
}
if let Some(last_sec) = self.sections.last() {
if let Some(last_line) = last_sec.lines.last() {
return Some(WordPlace {
section: self.sections.len() - 1,
line: last_sec.lines.len() - 1,
word: last_line.word_count.saturating_sub(1),
});
}
}
None
}
pub fn word_place_to_index(&self, place: &WordPlace) -> usize {
let mut index = 0;
for (si, section) in self.sections.iter().enumerate() {
if si < place.section {
index += section.words.len();
index += 1; } else {
if let Some(line) = section.lines.get(place.line) {
index += line.word_start + place.word;
}
break;
}
}
index
}
pub fn insert(&mut self, place: &WordPlace, text: &str) -> DocResult<()> {
let si = place.section;
if si >= self.sections.len() {
return Err(DocError::InvalidIndex(si));
}
let abs_idx = {
let section = &self.sections[si];
if section.lines.is_empty() {
0
} else {
let li = place.line.min(section.lines.len() - 1);
let line = §ion.lines[li];
let wi = place.word.min(line.word_count);
line.word_start + wi
}
};
let font_index = self.sections[si]
.words
.get(abs_idx)
.or_else(|| self.sections[si].words.get(abs_idx.saturating_sub(1)))
.map(|w| w.font_index)
.unwrap_or(0);
for (offset, ch) in text.chars().enumerate() {
self.sections[si].words.insert(
abs_idx + offset,
WordInfo {
character: ch,
font_index,
position_x: 0.0,
width: 0.0,
},
);
}
self.rearrange();
Ok(())
}
pub fn delete(&mut self, range: &WordRange) -> DocResult<()> {
let si = range.begin.section;
if si >= self.sections.len() {
return Err(DocError::InvalidIndex(si));
}
if range.end.section != si {
return Err(DocError::TypeMismatch {
expected: "same-section range".into(),
got: "cross-section range".into(),
});
}
let begin_abs = {
let section = &self.sections[si];
if section.lines.is_empty() {
return Ok(());
}
let li = range.begin.line.min(section.lines.len() - 1);
let line = §ion.lines[li];
line.word_start + range.begin.word.min(line.word_count)
};
let end_abs = {
let section = &self.sections[si];
let li = range.end.line.min(section.lines.len() - 1);
let line = §ion.lines[li];
(line.word_start + range.end.word.min(line.word_count)).min(section.words.len())
};
if begin_abs < end_abs {
self.sections[si].words.drain(begin_abs..end_abs);
self.rearrange();
}
Ok(())
}
pub fn iter_lines(&self) -> impl Iterator<Item = (usize, &Line)> {
self.sections
.iter()
.flat_map(|section| section.lines.iter())
.enumerate()
}
pub fn iter_words(&self) -> impl Iterator<Item = &WordInfo> {
self.sections
.iter()
.flat_map(|section| section.words.iter())
}
pub fn total_lines(&self) -> usize {
self.sections.iter().map(|s| s.lines.len()).sum()
}
pub fn total_words(&self) -> usize {
self.sections.iter().map(|s| s.words.len()).sum()
}
pub fn get_total_words(&self) -> i32 {
self.total_words() as i32
}
pub fn begin_word_place(&self) -> WordPlace {
WordPlace::default()
}
#[inline]
pub fn get_begin_word_place(&self) -> WordPlace {
self.begin_word_place()
}
pub fn end_word_place(&self) -> WordPlace {
if self.sections.is_empty() {
return WordPlace::default();
}
let si = self.sections.len() - 1;
let section = &self.sections[si];
if section.lines.is_empty() {
return WordPlace {
section: si,
line: 0,
word: 0,
};
}
let li = section.lines.len() - 1;
let word_count = section.lines[li].word_count;
WordPlace {
section: si,
line: li,
word: word_count.saturating_sub(1),
}
}
#[inline]
pub fn get_end_word_place(&self) -> WordPlace {
self.end_word_place()
}
pub fn prev_word_place(&self, place: &WordPlace) -> WordPlace {
if self.sections.is_empty() {
return *place;
}
let mut si = place.section.min(self.sections.len() - 1);
let section = &self.sections[si];
if section.lines.is_empty() {
if si == 0 {
return WordPlace::default();
}
si -= 1;
return self.section_end_place_for(si);
}
let mut li = place.line.min(section.lines.len() - 1);
let mut wi = place.word;
if wi > 0 {
return WordPlace {
section: si,
line: li,
word: wi - 1,
};
}
if li > 0 {
li -= 1;
wi = self.sections[si].lines[li].word_count.saturating_sub(1);
return WordPlace {
section: si,
line: li,
word: wi,
};
}
if si > 0 {
si -= 1;
return self.section_end_place_for(si);
}
WordPlace::default()
}
#[inline]
pub fn get_prev_word_place(&self, place: &WordPlace) -> WordPlace {
self.prev_word_place(place)
}
pub fn next_word_place(&self, place: &WordPlace) -> WordPlace {
if self.sections.is_empty() {
return *place;
}
let si = place.section.min(self.sections.len() - 1);
let section = &self.sections[si];
if section.lines.is_empty() {
if si + 1 < self.sections.len() {
return WordPlace {
section: si + 1,
line: 0,
word: 0,
};
}
return *place;
}
let li = place.line.min(section.lines.len() - 1);
let line = §ion.lines[li];
let wi = place.word;
if wi + 1 < line.word_count {
return WordPlace {
section: si,
line: li,
word: wi + 1,
};
}
if li + 1 < section.lines.len() {
return WordPlace {
section: si,
line: li + 1,
word: 0,
};
}
if si + 1 < self.sections.len() {
return WordPlace {
section: si + 1,
line: 0,
word: 0,
};
}
*place
}
#[inline]
pub fn get_next_word_place(&self, place: &WordPlace) -> WordPlace {
self.next_word_place(place)
}
pub fn search_word_place(&self, point: (f32, f32)) -> WordPlace {
self.hit_test(point.0, point.1)
.unwrap_or_else(|| self.end_word_place())
}
pub fn up_word_place(&self, place: &WordPlace, point: (f32, f32)) -> WordPlace {
if self.sections.is_empty() {
return *place;
}
let si = place.section.min(self.sections.len() - 1);
let section = &self.sections[si];
if section.lines.is_empty() {
return *place;
}
let li = place.line.min(section.lines.len() - 1);
let (target_si, target_li) = if li > 0 {
(si, li - 1)
} else if si > 0 {
let prev_si = si - 1;
let prev_lines = self.sections[prev_si].lines.len();
if prev_lines == 0 {
return *place;
}
(prev_si, prev_lines - 1)
} else {
return *place;
};
self.closest_word_on_line(target_si, target_li, point.0)
}
#[inline]
pub fn get_up_word_place(&self, place: &WordPlace, point: (f32, f32)) -> WordPlace {
self.up_word_place(place, point)
}
pub fn down_word_place(&self, place: &WordPlace, point: (f32, f32)) -> WordPlace {
if self.sections.is_empty() {
return *place;
}
let si = place.section.min(self.sections.len() - 1);
let section = &self.sections[si];
if section.lines.is_empty() {
return *place;
}
let li = place.line.min(section.lines.len() - 1);
let (target_si, target_li) = if li + 1 < section.lines.len() {
(si, li + 1)
} else if si + 1 < self.sections.len() {
let next_si = si + 1;
if self.sections[next_si].lines.is_empty() {
return *place;
}
(next_si, 0)
} else {
return *place;
};
self.closest_word_on_line(target_si, target_li, point.0)
}
#[inline]
pub fn get_down_word_place(&self, place: &WordPlace, point: (f32, f32)) -> WordPlace {
self.down_word_place(place, point)
}
pub fn line_begin_place(&self, place: &WordPlace) -> WordPlace {
if self.sections.is_empty() {
return *place;
}
let si = place.section.min(self.sections.len() - 1);
let section = &self.sections[si];
if section.lines.is_empty() {
return WordPlace {
section: si,
line: 0,
word: 0,
};
}
let li = place.line.min(section.lines.len() - 1);
WordPlace {
section: si,
line: li,
word: 0,
}
}
#[inline]
pub fn get_line_begin_place(&self, place: &WordPlace) -> WordPlace {
self.line_begin_place(place)
}
pub fn line_end_place(&self, place: &WordPlace) -> WordPlace {
if self.sections.is_empty() {
return *place;
}
let si = place.section.min(self.sections.len() - 1);
let section = &self.sections[si];
if section.lines.is_empty() {
return WordPlace {
section: si,
line: 0,
word: 0,
};
}
let li = place.line.min(section.lines.len() - 1);
let word_count = section.lines[li].word_count;
WordPlace {
section: si,
line: li,
word: word_count.saturating_sub(1),
}
}
#[inline]
pub fn get_line_end_place(&self, place: &WordPlace) -> WordPlace {
self.line_end_place(place)
}
pub fn section_begin_place(&self, place: &WordPlace) -> WordPlace {
let si = if self.sections.is_empty() {
0
} else {
place.section.min(self.sections.len() - 1)
};
WordPlace {
section: si,
line: 0,
word: 0,
}
}
#[inline]
pub fn get_section_begin_place(&self, place: &WordPlace) -> WordPlace {
self.section_begin_place(place)
}
pub fn section_end_place(&self, place: &WordPlace) -> WordPlace {
if self.sections.is_empty() {
return WordPlace::default();
}
let si = place.section.min(self.sections.len() - 1);
self.section_end_place_for(si)
}
#[inline]
pub fn get_section_end_place(&self, place: &WordPlace) -> WordPlace {
self.section_end_place(place)
}
pub fn update_word_place(&self, place: &mut WordPlace) {
if self.sections.is_empty() {
*place = WordPlace::default();
return;
}
place.section = place.section.min(self.sections.len() - 1);
let section = &self.sections[place.section];
if section.lines.is_empty() {
place.line = 0;
place.word = 0;
return;
}
place.line = place.line.min(section.lines.len() - 1);
let word_count = section.lines[place.line].word_count;
if word_count == 0 {
place.word = 0;
} else {
place.word = place.word.min(word_count - 1);
}
}
#[inline]
pub fn word_place_to_word_index(&self, place: &WordPlace) -> usize {
self.word_place_to_index(place)
}
pub fn word_index_to_word_place(&self, index: usize) -> WordPlace {
let mut remaining = index;
for (si, section) in self.sections.iter().enumerate() {
let words_in_section = section.words.len();
if remaining < words_in_section {
for (li, line) in section.lines.iter().enumerate() {
if remaining < line.word_start + line.word_count {
let word = remaining.saturating_sub(line.word_start);
return WordPlace {
section: si,
line: li,
word,
};
}
}
return self.section_end_place_for(si);
}
remaining -= words_in_section;
if remaining == 0 && si + 1 < self.sections.len() {
return WordPlace {
section: si + 1,
line: 0,
word: 0,
};
}
if si + 1 < self.sections.len() {
remaining = remaining.saturating_sub(1);
}
}
self.end_word_place()
}
pub fn plate_width(&self) -> f32 {
self.plate_rect[2] - self.plate_rect[0]
}
#[inline]
pub fn get_plate_width(&self) -> f32 {
self.plate_width()
}
pub fn plate_height(&self) -> f32 {
self.plate_rect[3] - self.plate_rect[1]
}
#[inline]
pub fn get_plate_height(&self) -> f32 {
self.plate_height()
}
pub fn begin_text_point(&self) -> (f32, f32) {
(self.plate_rect[0], self.plate_rect[3])
}
#[inline]
pub fn get_bt_point(&self) -> (f32, f32) {
self.begin_text_point()
}
pub fn end_text_point(&self) -> (f32, f32) {
(self.plate_rect[2], self.plate_rect[1])
}
#[inline]
pub fn get_et_point(&self) -> (f32, f32) {
self.end_text_point()
}
pub fn in_to_out(&self, point: (f32, f32)) -> (f32, f32) {
(point.0 + self.plate_rect[0], self.plate_rect[3] - point.1)
}
pub fn out_to_in(&self, point: (f32, f32)) -> (f32, f32) {
(point.0 - self.plate_rect[0], self.plate_rect[3] - point.1)
}
pub fn password_char(&self) -> Option<char> {
self.password_char
}
#[inline]
pub fn get_password_char(&self) -> Option<char> {
self.password_char()
}
#[deprecated(
since = "0.1.0",
note = "use password_char() or get_sub_word() instead"
)]
#[inline]
pub fn sub_word(&self) -> Option<char> {
self.password_char()
}
#[inline]
pub fn get_sub_word(&self) -> Option<char> {
self.password_char()
}
#[inline]
pub fn get_font_size(&self) -> f32 {
self.font_size()
}
pub fn alignment(&self) -> Alignment {
self.alignment
}
#[inline]
pub fn get_alignment(&self) -> Alignment {
self.alignment()
}
pub fn is_multi_line(&self) -> bool {
self.multi_line
}
pub fn is_auto_return(&self) -> bool {
self.auto_return
}
pub fn char_array(&self) -> Option<usize> {
self.char_array
}
#[inline]
pub fn get_char_array(&self) -> Option<usize> {
self.char_array()
}
pub fn limit_char(&self) -> Option<usize> {
self.limit_char
}
#[inline]
pub fn get_limit_char(&self) -> Option<usize> {
self.limit_char()
}
pub fn line_leading(&self) -> f32 {
let asc = self.provider.ascent(0, self.font_size);
let desc = self.provider.descent(0, self.font_size);
asc - desc
}
#[inline]
pub fn get_line_leading(&self) -> f32 {
self.line_leading()
}
pub fn line_ascent(&self) -> f32 {
self.provider.ascent(0, self.font_size)
}
pub fn line_descent(&self) -> f32 {
self.provider.descent(0, self.font_size)
}
pub fn word_iterator(&self) -> VtWordIterator<'_, P> {
VtWordIterator::new(self)
}
fn section_end_place_for(&self, si: usize) -> WordPlace {
if si >= self.sections.len() {
return self.end_word_place();
}
let section = &self.sections[si];
if section.lines.is_empty() {
return WordPlace {
section: si,
line: 0,
word: 0,
};
}
let li = section.lines.len() - 1;
let word_count = section.lines[li].word_count;
WordPlace {
section: si,
line: li,
word: word_count.saturating_sub(1),
}
}
fn closest_word_on_line(&self, si: usize, li: usize, x: f32) -> WordPlace {
let section = &self.sections[si];
let line = §ion.lines[li];
if line.word_count == 0 {
return WordPlace {
section: si,
line: li,
word: 0,
};
}
let rel_x = x - self.plate_rect[0] - line.x;
let end = line.word_start + line.word_count;
for wi in line.word_start..end {
let word = §ion.words[wi];
if rel_x <= word.position_x + word.width / 2.0 {
return WordPlace {
section: si,
line: li,
word: wi - line.word_start,
};
}
}
WordPlace {
section: si,
line: li,
word: line.word_count - 1,
}
}
fn find_auto_font_size(&self) -> f32 {
let plate_width = self.plate_rect[2] - self.plate_rect[0];
let plate_height = self.plate_rect[3] - self.plate_rect[1];
if plate_width <= 0.0 || plate_height <= 0.0 {
return 1.0;
}
let total_chars: usize = self.sections.iter().map(|s| s.words.len()).sum();
if total_chars == 0 {
return self.font_size;
}
let mut best = AUTO_FONT_SIZES[0];
for &size in AUTO_FONT_SIZES {
if self.text_fits_at_size(size, plate_width, plate_height) {
best = size;
} else {
break;
}
}
best
}
fn text_fits_at_size(&self, size: f32, plate_width: f32, plate_height: f32) -> bool {
let ascent = self.provider.ascent(0, size);
let descent = self.provider.descent(0, size);
let line_height = ascent - descent;
if line_height <= 0.0 {
return false;
}
let mut total_lines = 0_usize;
for section in &self.sections {
if section.words.is_empty() {
total_lines += 1;
continue;
}
let mut line_width = 0.0_f32;
let mut lines_in_section = 1_usize;
for word in §ion.words {
let w = self
.provider
.char_width(word.font_index, word.character as u16, size);
if self.multi_line
&& self.auto_return
&& line_width + w > plate_width
&& line_width > 0.0
{
lines_in_section += 1;
line_width = w;
} else {
line_width += w;
}
if !self.multi_line && line_width > plate_width {
return false;
}
}
total_lines += lines_in_section;
}
let total_height = total_lines as f32 * line_height;
total_height <= plate_height
}
}
pub struct VtWordIterator<'a, P: VtFontProvider> {
vt: &'a VariableText<P>,
current: WordPlace,
}
impl<'a, P: VtFontProvider> VtWordIterator<'a, P> {
pub fn new(vt: &'a VariableText<P>) -> Self {
Self {
vt,
current: vt.begin_word_place(),
}
}
pub fn set_at_index(&mut self, word_index: i32) {
let idx = if word_index < 0 {
0
} else {
word_index as usize
};
self.current = self.vt.word_index_to_word_place(idx);
}
pub fn set_at_place(&mut self, place: WordPlace) {
self.current = place;
}
pub fn next_word(&mut self) -> bool {
let next = self.vt.next_word_place(&self.current);
if next == self.current {
return false;
}
self.current = next;
true
}
pub fn next_line(&mut self) -> bool {
if self.vt.sections.is_empty() {
return false;
}
let si = self.current.section.min(self.vt.sections.len() - 1);
let section = &self.vt.sections[si];
if section.lines.is_empty() {
return false;
}
let li = self.current.line.min(section.lines.len() - 1);
if li + 1 < section.lines.len() {
self.current = WordPlace {
section: si,
line: li + 1,
word: 0,
};
return true;
}
if si + 1 < self.vt.sections.len() {
self.current = WordPlace {
section: si + 1,
line: 0,
word: 0,
};
return true;
}
false
}
pub fn word_place(&self) -> &WordPlace {
&self.current
}
#[inline]
pub fn get_word_place(&self) -> &WordPlace {
self.word_place()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct FixedWidthProvider {
char_width: f32,
ascent: f32,
descent: f32,
}
impl FixedWidthProvider {
fn new(char_width: f32) -> Self {
Self {
char_width,
ascent: 10.0,
descent: -3.0,
}
}
}
impl VtFontProvider for FixedWidthProvider {
fn char_width(&self, _font_index: usize, _word: u16, font_size: f32) -> f32 {
self.char_width * (font_size / 12.0)
}
fn ascent(&self, _font_index: usize, font_size: f32) -> f32 {
self.ascent * (font_size / 12.0)
}
fn descent(&self, _font_index: usize, font_size: f32) -> f32 {
self.descent * (font_size / 12.0)
}
}
fn make_vt(provider: FixedWidthProvider) -> VariableText<FixedWidthProvider> {
let mut vt = VariableText::new(provider);
vt.set_plate_rect([0.0, 0.0, 200.0, 100.0]);
vt.set_font_size(12.0);
vt
}
#[test]
fn test_empty_text() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("");
vt.rearrange();
assert_eq!(vt.sections().len(), 1);
assert_eq!(vt.sections()[0].words.len(), 0);
assert_eq!(vt.sections()[0].lines.len(), 1);
}
#[test]
fn test_single_line_text() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Hello");
vt.rearrange();
assert_eq!(vt.sections().len(), 1);
assert_eq!(vt.sections()[0].words.len(), 5);
assert_eq!(vt.sections()[0].lines.len(), 1);
assert_eq!(vt.sections()[0].lines[0].word_count, 5);
}
#[test]
fn test_multi_section_text() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Line1\nLine2\nLine3");
vt.rearrange();
assert_eq!(vt.sections().len(), 3);
assert_eq!(vt.sections()[0].words.len(), 5);
assert_eq!(vt.sections()[1].words.len(), 5);
assert_eq!(vt.sections()[2].words.len(), 5);
}
#[test]
fn test_line_breaking() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_multi_line(true);
vt.set_auto_return(true);
let text: String = "A".repeat(50);
vt.set_text(&text);
vt.rearrange();
assert_eq!(vt.sections()[0].lines.len(), 2);
}
#[test]
fn test_no_line_breaking_single_line_mode() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_multi_line(false);
let text: String = "A".repeat(50);
vt.set_text(&text);
vt.rearrange();
assert_eq!(vt.sections()[0].lines.len(), 1);
assert_eq!(vt.sections()[0].lines[0].word_count, 50);
}
#[test]
fn test_alignment_left() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_alignment(Alignment::Left);
vt.set_text("Hi");
vt.rearrange();
assert!((vt.sections()[0].lines[0].x - 0.0).abs() < 0.01);
}
#[test]
fn test_alignment_center() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_alignment(Alignment::Center);
vt.set_text("Hi");
vt.rearrange();
let line = &vt.sections()[0].lines[0];
assert!((line.x - 94.0).abs() < 0.01);
}
#[test]
fn test_alignment_right() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_alignment(Alignment::Right);
vt.set_text("Hi");
vt.rearrange();
let line = &vt.sections()[0].lines[0];
assert!((line.x - 188.0).abs() < 0.01);
}
#[test]
fn test_alignment_from_value() {
assert_eq!(Alignment::from_value(0), Alignment::Left);
assert_eq!(Alignment::from_value(1), Alignment::Center);
assert_eq!(Alignment::from_value(2), Alignment::Right);
assert_eq!(Alignment::from_value(99), Alignment::Left);
}
#[test]
fn test_char_array_mode() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_char_array(Some(10));
vt.set_text("ABC");
vt.rearrange();
let section = &vt.sections()[0];
assert_eq!(section.lines.len(), 1);
assert_eq!(section.lines[0].word_count, 3);
let first_word = §ion.words[0];
let expected_x = (20.0 - 6.0) / 2.0; assert!((first_word.position_x - expected_x).abs() < 0.01);
}
#[test]
fn test_char_array_limits_to_count() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_char_array(Some(3));
vt.set_text("ABCDE");
vt.rearrange();
assert_eq!(vt.sections()[0].lines[0].word_count, 3);
}
#[test]
fn test_password_masking() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_password_char(Some('*'));
vt.set_text("secret");
vt.rearrange();
let section = &vt.sections()[0];
assert_eq!(section.words.len(), 6);
for word in §ion.words {
assert_eq!(word.character, '*');
}
}
#[test]
fn test_limit_char() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_limit_char(Some(5));
vt.set_text("Hello World");
vt.rearrange();
assert_eq!(vt.sections()[0].words.len(), 5);
}
#[test]
fn test_auto_font_size() {
let mut vt = VariableText::new(FixedWidthProvider::new(6.0));
vt.set_plate_rect([0.0, 0.0, 100.0, 20.0]);
vt.set_auto_font_size(true);
vt.set_text("Hello World This Is Long");
vt.rearrange();
let content_height = vt.content_rect()[3] - vt.content_rect()[1];
assert!(content_height <= 20.0 + 1.0); }
#[test]
fn test_auto_font_size_empty() {
let mut vt = VariableText::new(FixedWidthProvider::new(6.0));
vt.set_plate_rect([0.0, 0.0, 100.0, 20.0]);
vt.set_font_size(12.0);
vt.set_auto_font_size(true);
vt.set_text("");
vt.rearrange();
assert_eq!(vt.font_size(), 12.0);
}
#[test]
fn test_hit_test_first_char() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Hello");
vt.rearrange();
let line = &vt.sections()[0].lines[0];
let y = line.y + line.ascent / 2.0;
let place = vt.hit_test(1.0, y);
assert!(place.is_some());
assert_eq!(place.unwrap().word, 0);
}
#[test]
fn test_hit_test_past_end() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Hi");
vt.rearrange();
let line = &vt.sections()[0].lines[0];
let y = line.y + line.ascent / 2.0;
let place = vt.hit_test(100.0, y);
assert!(place.is_some());
assert_eq!(place.unwrap().word, 1); }
#[test]
fn test_word_place_to_index_first() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Hello");
vt.rearrange();
let idx = vt.word_place_to_index(&WordPlace {
section: 0,
line: 0,
word: 0,
});
assert_eq!(idx, 0);
}
#[test]
fn test_word_place_to_index_second_section() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("AB\nCD");
vt.rearrange();
let idx = vt.word_place_to_index(&WordPlace {
section: 1,
line: 0,
word: 0,
});
assert_eq!(idx, 3); }
#[test]
fn test_word_place_to_index_mid() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Hello");
vt.rearrange();
let idx = vt.word_place_to_index(&WordPlace {
section: 0,
line: 0,
word: 3,
});
assert_eq!(idx, 3);
}
#[test]
fn test_content_rect_non_empty() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Hello");
vt.rearrange();
let cr = vt.content_rect();
assert!(cr[3] > cr[1]); }
#[test]
fn test_multi_line_many_breaks() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_multi_line(true);
vt.set_auto_return(true);
let text: String = "X".repeat(100);
vt.set_text(&text);
vt.rearrange();
let line_count = vt.sections()[0].lines.len();
assert!(line_count >= 3);
assert!(line_count <= 4);
}
#[test]
fn test_word_info_positions_increase() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("ABCDE");
vt.rearrange();
let words = &vt.sections()[0].words;
for i in 1..words.len() {
assert!(words[i].position_x > words[i - 1].position_x);
}
}
#[test]
fn test_cjk_characters_layout() {
let mut vt = VariableText::new(FixedWidthProvider::new(12.0));
vt.set_plate_rect([0.0, 0.0, 100.0, 100.0]);
vt.set_font_size(12.0);
vt.set_multi_line(true);
vt.set_auto_return(true);
let text: String = "\u{4E00}".repeat(15);
vt.set_text(&text);
vt.rearrange();
assert!(vt.sections()[0].lines.len() >= 2);
}
#[test]
fn test_multi_line_auto_font_size() {
let mut vt = VariableText::new(FixedWidthProvider::new(6.0));
vt.set_plate_rect([0.0, 0.0, 50.0, 30.0]);
vt.set_multi_line(true);
vt.set_auto_return(true);
vt.set_auto_font_size(true);
vt.set_text("This is a fairly long text that needs to fit");
vt.rearrange();
assert!(vt.font_size() < 12.0); assert!(vt.font_size() >= 4.0); }
#[test]
fn test_line_y_positions_decrease() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Line1\nLine2\nLine3");
vt.rearrange();
let y0 = vt.sections()[0].lines[0].y;
let y1 = vt.sections()[1].lines[0].y;
let y2 = vt.sections()[2].lines[0].y;
assert!(y0 > y1);
assert!(y1 > y2);
}
#[test]
fn test_hit_test_below_all_lines() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Hello");
vt.rearrange();
let place = vt.hit_test(5.0, -100.0);
assert!(place.is_some());
}
#[test]
fn test_word_range_creation() {
let range = WordRange {
begin: WordPlace {
section: 0,
line: 0,
word: 0,
},
end: WordPlace {
section: 0,
line: 0,
word: 5,
},
};
assert_eq!(range.begin.word, 0);
assert_eq!(range.end.word, 5);
}
#[test]
fn test_cr_line_endings() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Line1\rLine2\rLine3");
vt.rearrange();
assert_eq!(vt.sections().len(), 3);
assert_eq!(vt.sections()[0].words.len(), 5);
assert_eq!(vt.sections()[1].words.len(), 5);
assert_eq!(vt.sections()[2].words.len(), 5);
}
#[test]
fn test_crlf_line_endings() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Line1\r\nLine2\r\nLine3");
vt.rearrange();
assert_eq!(vt.sections().len(), 3);
assert_eq!(vt.sections()[0].words.len(), 5);
assert_eq!(vt.sections()[1].words.len(), 5);
assert_eq!(vt.sections()[2].words.len(), 5);
}
#[test]
fn test_mixed_line_endings() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("A\r\nB\rC\nD");
vt.rearrange();
assert_eq!(vt.sections().len(), 4);
assert_eq!(vt.sections()[0].words.len(), 1); assert_eq!(vt.sections()[1].words.len(), 1); assert_eq!(vt.sections()[2].words.len(), 1); assert_eq!(vt.sections()[3].words.len(), 1); }
#[test]
fn test_insert_adds_chars_and_rearranges() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Hello");
vt.rearrange();
let place = WordPlace {
section: 0,
line: 0,
word: 2,
};
vt.insert(&place, "XY").unwrap();
assert_eq!(vt.sections()[0].words.len(), 7);
assert_eq!(vt.sections()[0].words[2].character, 'X');
assert_eq!(vt.sections()[0].words[3].character, 'Y');
}
#[test]
fn test_delete_removes_chars_and_rearranges() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Hello");
vt.rearrange();
let range = WordRange {
begin: WordPlace {
section: 0,
line: 0,
word: 0,
},
end: WordPlace {
section: 0,
line: 0,
word: 3,
},
};
vt.delete(&range).unwrap();
assert_eq!(vt.sections()[0].words.len(), 2);
assert_eq!(vt.sections()[0].words[0].character, 'l');
assert_eq!(vt.sections()[0].words[1].character, 'o');
}
#[test]
fn test_iter_lines_count() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Line1\nLine2\nLine3");
vt.rearrange();
assert_eq!(vt.iter_lines().count(), 3);
assert_eq!(vt.total_lines(), 3);
}
#[test]
fn test_iter_words_count() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Hello");
vt.rearrange();
assert_eq!(vt.iter_words().count(), 5);
assert_eq!(vt.total_words(), 5);
}
#[test]
fn test_iter_words_across_sections() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("AB\nCD");
vt.rearrange();
assert_eq!(vt.iter_words().count(), 4);
assert_eq!(vt.total_words(), 4);
}
#[test]
fn test_total_words_empty() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("");
vt.rearrange();
assert_eq!(vt.total_words(), 0);
assert_eq!(vt.get_total_words(), 0);
}
#[test]
fn test_total_words_single() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("A");
vt.rearrange();
assert_eq!(vt.total_words(), 1);
}
#[test]
fn test_total_words_multi() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Hello\nWorld");
vt.rearrange();
assert_eq!(vt.total_words(), 10);
}
#[test]
fn test_begin_and_end_word_place_empty() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("");
vt.rearrange();
let begin = vt.begin_word_place();
assert_eq!(begin, WordPlace::default());
let end = vt.end_word_place();
assert_eq!(end.section, 0);
}
#[test]
fn test_begin_and_end_word_place_non_empty() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Hello");
vt.rearrange();
let begin = vt.begin_word_place();
assert_eq!(
begin,
WordPlace {
section: 0,
line: 0,
word: 0
}
);
let end = vt.end_word_place();
assert_eq!(end.section, 0);
assert_eq!(end.word, 4);
}
#[test]
fn test_word_place_to_word_index_round_trip() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("ABCDE");
vt.rearrange();
for i in 0..5 {
let place = WordPlace {
section: 0,
line: 0,
word: i,
};
let idx = vt.word_place_to_word_index(&place);
assert_eq!(idx, i, "index mismatch at word {i}");
let back = vt.word_index_to_word_place(idx);
assert_eq!(back, place, "round-trip mismatch at word {i}");
}
}
#[test]
fn test_word_index_to_place_across_sections() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("AB\nCD");
vt.rearrange();
let p0 = vt.word_index_to_word_place(0);
assert_eq!(
p0,
WordPlace {
section: 0,
line: 0,
word: 0
}
);
let p1 = vt.word_index_to_word_place(1);
assert_eq!(
p1,
WordPlace {
section: 0,
line: 0,
word: 1
}
);
let p3 = vt.word_index_to_word_place(3);
assert_eq!(p3.section, 1);
assert_eq!(p3.word, 0);
}
#[test]
fn test_prev_next_word_place_navigation() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("ABCDE");
vt.rearrange();
let mut place = vt.begin_word_place();
let mut count = 0;
loop {
count += 1;
let next = vt.next_word_place(&place);
if next == place {
break;
}
place = next;
}
assert_eq!(count, 5);
place = vt.end_word_place();
count = 0;
loop {
count += 1;
let prev = vt.prev_word_place(&place);
if prev == place {
break;
}
place = prev;
}
assert_eq!(count, 5);
}
#[test]
fn test_line_begin_and_end_place() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Hello");
vt.rearrange();
let mid = WordPlace {
section: 0,
line: 0,
word: 2,
};
let begin = vt.line_begin_place(&mid);
assert_eq!(begin.word, 0);
let end = vt.line_end_place(&mid);
assert_eq!(end.word, 4);
}
#[test]
fn test_section_begin_and_end_place() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Hello\nWorld");
vt.rearrange();
let mid_sec1 = WordPlace {
section: 1,
line: 0,
word: 2,
};
let sec_begin = vt.section_begin_place(&mid_sec1);
assert_eq!(
sec_begin,
WordPlace {
section: 1,
line: 0,
word: 0
}
);
let sec_end = vt.section_end_place(&mid_sec1);
assert_eq!(sec_end.section, 1);
assert_eq!(sec_end.word, 4);
}
#[test]
fn test_plate_width_and_height() {
let vt = make_vt(FixedWidthProvider::new(6.0));
assert!((vt.plate_width() - 200.0).abs() < 0.01);
assert!((vt.plate_height() - 100.0).abs() < 0.01);
assert!((vt.get_plate_width() - 200.0).abs() < 0.01);
assert!((vt.get_plate_height() - 100.0).abs() < 0.01);
}
#[test]
fn test_in_to_out_and_out_to_in_inverse() {
let vt = make_vt(FixedWidthProvider::new(6.0));
let original = (50.0_f32, 30.0_f32);
let out = vt.in_to_out(original);
let back = vt.out_to_in(out);
assert!((back.0 - original.0).abs() < 0.001, "x mismatch");
assert!((back.1 - original.1).abs() < 0.001, "y mismatch");
}
#[test]
fn test_vt_word_iterator_next_word_advances() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("ABCDE");
vt.rearrange();
let mut it = vt.word_iterator();
let mut count = 0;
while it.next_word() {
count += 1;
}
assert_eq!(count, 4);
}
#[test]
fn test_vt_word_iterator_set_at_index() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("ABCDE");
vt.rearrange();
let mut it = vt.word_iterator();
it.set_at_index(3);
assert_eq!(it.word_place().word, 3);
}
#[test]
fn test_vt_word_iterator_set_at_place() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Hello");
vt.rearrange();
let target = WordPlace {
section: 0,
line: 0,
word: 2,
};
let mut it = vt.word_iterator();
it.set_at_place(target);
assert_eq!(*it.get_word_place(), target);
}
#[test]
fn test_config_getters() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Test");
vt.rearrange();
assert!((vt.font_size() - 12.0).abs() < 0.01);
assert!((vt.get_font_size() - 12.0).abs() < 0.01);
assert_eq!(vt.alignment(), Alignment::Left);
assert_eq!(vt.get_alignment(), Alignment::Left);
assert!(!vt.is_multi_line());
assert!(vt.is_auto_return());
}
#[test]
fn test_password_char_getter() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
assert_eq!(vt.password_char(), None);
assert_eq!(vt.get_password_char(), None);
assert_eq!(vt.get_sub_word(), None);
vt.set_password_char(Some('*'));
assert_eq!(vt.password_char(), Some('*'));
assert_eq!(vt.get_sub_word(), Some('*'));
assert_eq!(vt.get_password_char(), Some('*'));
}
#[test]
fn test_line_leading_positive() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Hi");
vt.rearrange();
assert!((vt.line_leading() - 13.0).abs() < 0.01);
assert!((vt.get_line_leading() - 13.0).abs() < 0.01);
}
#[test]
fn test_update_word_place_clamping() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Hi");
vt.rearrange();
let mut out_of_bounds = WordPlace {
section: 99,
line: 99,
word: 99,
};
vt.update_word_place(&mut out_of_bounds);
assert_eq!(out_of_bounds.section, 0);
assert_eq!(out_of_bounds.line, 0);
assert!(out_of_bounds.word <= 1);
}
#[test]
fn test_search_word_place_returns_valid() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Hello");
vt.rearrange();
let line = &vt.sections()[0].lines[0];
let y = line.y + line.ascent / 2.0;
let place = vt.search_word_place((10.0, y));
assert_eq!(place.section, 0);
}
#[test]
fn test_vt_word_iterator_next_line_across_sections() {
let mut vt = make_vt(FixedWidthProvider::new(6.0));
vt.set_text("Hello\nWorld");
vt.rearrange();
let mut it = vt.word_iterator();
assert_eq!(it.word_place().section, 0);
assert!(it.next_line());
assert_eq!(it.word_place().section, 1);
assert_eq!(it.word_place().line, 0);
assert!(!it.next_line());
}
}