use unicode_segmentation::UnicodeSegmentation;
pub(crate) const MONOSPACE_CHAR_WIDTH_RATIO: f32 = 0.6;
const EMOJI_CHAR_WIDTH: f32 = 2.29;
pub(crate) fn compute_text_dimensions(
text: &str,
char_width: f32,
max_width: Option<f32>,
) -> (f32, usize) {
if text.is_empty() {
return (0.0, 0);
}
let max_chars_per_line = max_width.map(|w| (w / char_width).floor() as usize);
let mut line_width_max: f32 = 0.0;
let mut line_count: usize = 0;
text.lines().for_each(|line| {
let line_char_count = line.chars().count();
match max_chars_per_line {
Some(max_chars) if max_chars > 0 && line_char_count > max_chars => {
let wrapped = wrap_line_monospace(line, max_chars);
wrapped.into_iter().for_each(|wrapped_line| {
let width = line_width_measure(wrapped_line, char_width);
line_width_max = line_width_max.max(width);
line_count += 1;
});
}
_ => {
let width = line_width_measure(line, char_width);
line_width_max = line_width_max.max(width);
line_count += 1;
}
}
});
(line_width_max, line_count)
}
pub(crate) fn line_width_measure(line: &str, char_width: f32) -> f32 {
if line.is_empty() {
return 0.0;
}
let mut line_char_column_count = line
.graphemes(true)
.map(|grapheme| match emojis::get(grapheme).is_some() {
true => EMOJI_CHAR_WIDTH,
false => 1.0f32,
})
.sum::<f32>();
line_char_column_count += 1.0;
line_char_column_count * char_width
}
pub(crate) fn wrap_text_monospace(text: &str, char_width: f32, max_width: f32) -> Vec<String> {
let max_chars = (max_width / char_width).floor() as usize;
if max_chars == 0 {
return text.lines().map(String::from).collect();
}
let mut result = Vec::new();
text.lines().for_each(|line| {
let wrapped = wrap_line_monospace(line, max_chars);
result.extend(wrapped.into_iter().map(String::from));
});
if result.is_empty() {
result.push(String::new());
}
result
}
pub(crate) fn wrap_line_monospace(line: &str, max_chars: usize) -> Vec<&str> {
if max_chars == 0 {
return vec![line];
}
let mut result = Vec::new();
let mut remaining = line;
while !remaining.is_empty() {
let char_count = remaining.chars().count();
if char_count <= max_chars {
result.push(remaining);
break;
}
let mut break_at_byte = 0;
let mut break_at_char = 0;
let mut last_space_byte = None;
let mut last_space_char = 0;
remaining
.char_indices()
.enumerate()
.for_each(|(char_idx, (byte_idx, c))| {
if char_idx >= max_chars {
return;
}
if c.is_whitespace() {
last_space_byte = Some(byte_idx);
last_space_char = char_idx;
}
break_at_byte = byte_idx + c.len_utf8();
break_at_char = char_idx + 1;
});
let (split_byte, split_char) =
if let Some(space_byte) = last_space_byte.filter(|_| last_space_char > max_chars / 2) {
(space_byte, last_space_char)
} else {
(break_at_byte, break_at_char)
};
if split_char == 0 {
result.push(remaining);
break;
}
result.push(&remaining[..split_byte]);
remaining = remaining[split_byte..].trim_start();
}
if result.is_empty() {
result.push("");
}
result
}