use teksilo_tokens::TextStyle;
use crate::text_backend::{EllipsisMode, TextBackend};
pub const ELLIPSIS: char = '\u{2026}';
pub fn ellipsize(
text: &str,
style: &TextStyle,
max_width: f32,
mode: EllipsisMode,
backend: &mut dyn TextBackend,
) -> String {
if text.is_empty() || max_width <= 0.0 {
return String::new();
}
let full_width = measure(text, style, backend);
if full_width <= max_width {
return text.to_string();
}
match mode {
EllipsisMode::Trailing => text.to_string(),
EllipsisMode::Middle => middle_ellipsize(text, style, max_width, backend),
EllipsisMode::Leading => leading_ellipsize(text, style, max_width, backend),
}
}
fn measure(text: &str, style: &TextStyle, backend: &mut dyn TextBackend) -> f32 {
if text.is_empty() {
return 0.0;
}
backend.layout_single_line(text, style, None).width
}
fn ellipsis_str() -> String {
let mut s = String::new();
s.push(ELLIPSIS);
s
}
fn middle_ellipsize(
text: &str,
style: &TextStyle,
max_width: f32,
backend: &mut dyn TextBackend,
) -> String {
let boundaries: Vec<usize> = text
.char_indices()
.map(|(i, _)| i)
.chain(std::iter::once(text.len()))
.collect();
let n = boundaries.len().saturating_sub(1);
if n == 0 {
return String::new();
}
for total_kept in (0..n).rev() {
let head_len = total_kept.div_ceil(2);
let tail_len = total_kept - head_len;
let head_end_byte = boundaries[head_len];
let tail_start_byte = boundaries[n - tail_len];
let mut candidate =
String::with_capacity(head_end_byte + 4 + (text.len() - tail_start_byte));
candidate.push_str(&text[..head_end_byte]);
candidate.push(ELLIPSIS);
candidate.push_str(&text[tail_start_byte..]);
if measure(&candidate, style, backend) <= max_width {
return candidate;
}
}
ellipsis_str()
}
fn leading_ellipsize(
text: &str,
style: &TextStyle,
max_width: f32,
backend: &mut dyn TextBackend,
) -> String {
let boundaries: Vec<usize> = text
.char_indices()
.map(|(i, _)| i)
.chain(std::iter::once(text.len()))
.collect();
if boundaries.len() <= 1 {
return String::new();
}
for &start_byte in boundaries.iter().skip(1) {
let mut candidate = String::with_capacity(4 + (text.len() - start_byte));
candidate.push(ELLIPSIS);
candidate.push_str(&text[start_byte..]);
if measure(&candidate, style, backend) <= max_width {
return candidate;
}
}
ellipsis_str()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::text_backend::MockTextBackend;
fn style() -> TextStyle {
TextStyle::default()
}
#[test]
fn short_text_fits_unchanged() {
let mut backend = MockTextBackend::new();
let s = ellipsize("Hi", &style(), 100.0, EllipsisMode::Middle, &mut backend);
assert_eq!(s, "Hi");
}
#[test]
fn trailing_is_shim_passthrough() {
let mut backend = MockTextBackend::new();
let s = ellipsize(
"Hello World",
&style(),
20.0,
EllipsisMode::Trailing,
&mut backend,
);
assert_eq!(s, "Hello World");
}
#[test]
fn middle_inserts_ellipsis_and_preserves_ends() {
let mut backend = MockTextBackend::new();
let s = ellipsize(
"Hello beautiful world",
&style(),
80.0,
EllipsisMode::Middle,
&mut backend,
);
assert!(s.contains(ELLIPSIS), "expected ellipsis in {s:?}");
assert!(s.starts_with('H'), "head preserved in {s:?}");
assert!(s.ends_with('d'), "tail preserved in {s:?}");
let w = measure(&s, &style(), &mut backend);
assert!(w <= 80.0, "width {w} exceeds budget 80");
}
#[test]
fn leading_starts_with_ellipsis() {
let mut backend = MockTextBackend::new();
let s = ellipsize(
"Hello beautiful world",
&style(),
80.0,
EllipsisMode::Leading,
&mut backend,
);
assert!(
s.starts_with(ELLIPSIS),
"expected leading ellipsis in {s:?}"
);
assert!(s.ends_with('d'), "tail preserved in {s:?}");
let w = measure(&s, &style(), &mut backend);
assert!(w <= 80.0, "width {w} exceeds budget 80");
}
#[test]
fn empty_text_returns_empty() {
let mut backend = MockTextBackend::new();
let s = ellipsize("", &style(), 100.0, EllipsisMode::Middle, &mut backend);
assert_eq!(s, "");
}
#[test]
fn tiny_budget_returns_bare_ellipsis() {
let mut backend = MockTextBackend::new();
let s = ellipsize("Hello", &style(), 4.0, EllipsisMode::Middle, &mut backend);
assert!(s.is_empty() || s == ellipsis_str());
}
#[test]
fn leading_picks_longest_fitting_suffix() {
let mut backend = MockTextBackend::new();
let s = ellipsize(
"ABCDEFGHIJ",
&style(),
40.0,
EllipsisMode::Leading,
&mut backend,
);
assert_eq!(s, "\u{2026}IJ");
}
}