use super::font::FontDesc;
use super::text::{TextMetrics, TextRenderer, TextStyle};
use super::{Color, Rect};
use anyhow::Result;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Alignment {
Left,
Center,
Right,
Justify,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum VerticalAlignment {
Top,
Middle,
Bottom,
Baseline,
}
#[derive(Debug, Clone)]
pub struct TextLayout {
text: String,
style: TextStyle,
bounds: Option<Rect>,
alignment: Alignment,
vertical_alignment: VerticalAlignment,
line_spacing: f64,
wrap: bool,
ellipsis: bool,
}
impl TextLayout {
pub fn new() -> Self {
Self {
text: String::new(),
style: TextStyle::default(),
bounds: None,
alignment: Alignment::Left,
vertical_alignment: VerticalAlignment::Baseline,
line_spacing: 1.2,
wrap: false,
ellipsis: false,
}
}
pub fn text<S: Into<String>>(mut self, text: S) -> Self {
self.text = text.into();
self
}
pub fn style(mut self, style: TextStyle) -> Self {
self.style = style;
self
}
pub fn font(mut self, font: FontDesc) -> Self {
self.style.font = font;
self
}
pub fn color(mut self, color: Color) -> Self {
self.style.color = color;
self
}
pub fn background(mut self, color: Color) -> Self {
self.style.background = Some(color);
self
}
pub fn bounds(mut self, bounds: Rect) -> Self {
self.bounds = Some(bounds);
self
}
pub fn alignment(mut self, alignment: Alignment) -> Self {
self.alignment = alignment;
self
}
pub fn vertical_alignment(mut self, alignment: VerticalAlignment) -> Self {
self.vertical_alignment = alignment;
self
}
pub fn line_spacing(mut self, spacing: f64) -> Self {
self.line_spacing = spacing;
self
}
pub fn wrap(mut self, wrap: bool) -> Self {
self.wrap = wrap;
self
}
pub fn ellipsis(mut self, ellipsis: bool) -> Self {
self.ellipsis = ellipsis;
self
}
pub fn get_text(&self) -> &str {
&self.text
}
pub fn get_font(&self) -> &FontDesc {
&self.style.font
}
pub fn get_color(&self) -> &Color {
&self.style.color
}
pub fn measure(&self, renderer: &TextRenderer) -> Result<TextMetrics> {
if let Some(bounds) = self.bounds {
if self.wrap {
let max_width = bounds.width as f64;
let lines = self.get_wrapped_lines(renderer, max_width)?;
let line_metrics = renderer.measure_text("M", &self.style)?;
let line_height = line_metrics.ascent + line_metrics.descent;
let total_height = lines.len() as f64 * line_height * self.line_spacing;
let max_line_width = lines
.iter()
.map(|line| renderer.measure_text(line, &self.style))
.collect::<Result<Vec<_>>>()?
.iter()
.map(|m| m.width)
.fold(0.0, f64::max);
Ok(TextMetrics {
width: max_line_width.min(max_width),
height: total_height,
ascent: line_metrics.ascent,
descent: line_metrics.descent,
x_advance: max_line_width,
y_advance: total_height,
})
} else {
renderer.measure_text(&self.text, &self.style)
}
} else {
renderer.measure_text(&self.text, &self.style)
}
}
pub fn render(&self, renderer: &TextRenderer, x: i32, y: i32) -> Result<()> {
if self.text.is_empty() {
return Ok(());
}
let render_x = x as f64;
let render_y = y as f64;
if let Some(bounds) = self.bounds {
if self.wrap {
self.render_wrapped(renderer, bounds, render_x, render_y)
} else {
self.render_single_line(renderer, bounds, render_x, render_y)
}
} else {
renderer.render_text(&self.text, render_x, render_y, &self.style)
}
}
fn render_single_line(
&self,
renderer: &TextRenderer,
bounds: Rect,
x: f64,
y: f64,
) -> Result<()> {
let text = if self.ellipsis {
self.apply_ellipsis(renderer, bounds.width as f64)?
} else {
self.text.clone()
};
let metrics = renderer.measure_text(&text, &self.style)?;
let (aligned_x, aligned_y) = self.apply_alignment(x, y, &metrics, bounds);
renderer.render_text(&text, aligned_x, aligned_y, &self.style)
}
fn render_wrapped(&self, renderer: &TextRenderer, bounds: Rect, x: f64, y: f64) -> Result<()> {
let max_width = bounds.width as f64;
let lines = self.get_wrapped_lines(renderer, max_width)?;
if lines.is_empty() {
return Ok(());
}
let line_metrics = renderer.measure_text("M", &self.style)?;
let line_height = (line_metrics.ascent + line_metrics.descent) * self.line_spacing;
let total_height = lines.len() as f64 * line_height;
let start_y = match self.vertical_alignment {
VerticalAlignment::Top => y + line_metrics.ascent,
VerticalAlignment::Middle => {
y + (bounds.height as f64 - total_height) / 2.0 + line_metrics.ascent
}
VerticalAlignment::Bottom => {
y + bounds.height as f64 - total_height + line_metrics.ascent
}
VerticalAlignment::Baseline => y,
};
for (i, line) in lines.iter().enumerate() {
let line_metrics = renderer.measure_text(line, &self.style)?;
let line_y = start_y + i as f64 * line_height;
let line_x = match self.alignment {
Alignment::Left => x,
Alignment::Center => x + (bounds.width as f64 - line_metrics.width) / 2.0,
Alignment::Right => x + bounds.width as f64 - line_metrics.width,
Alignment::Justify => {
if i == lines.len() - 1 || line.split_whitespace().count() <= 1 {
x
} else {
self.render_justified_line(renderer, line, x, line_y, bounds.width as f64)?;
continue;
}
}
};
renderer.render_text(line, line_x, line_y, &self.style)?;
}
Ok(())
}
fn render_justified_line(
&self,
renderer: &TextRenderer,
line: &str,
x: f64,
y: f64,
max_width: f64,
) -> Result<()> {
let words: Vec<&str> = line.split_whitespace().collect();
if words.len() <= 1 {
return renderer.render_text(line, x, y, &self.style);
}
let word_widths: Result<Vec<f64>> = words
.iter()
.map(|word| renderer.measure_text(word, &self.style).map(|m| m.width))
.collect();
let word_widths = word_widths?;
let total_word_width: f64 = word_widths.iter().sum();
let total_space_width = max_width - total_word_width;
let space_between_words = total_space_width / (words.len() - 1) as f64;
let mut current_x = x;
for (i, (word, &word_width)) in words.iter().zip(word_widths.iter()).enumerate() {
renderer.render_text(word, current_x, y, &self.style)?;
current_x += word_width;
if i < words.len() - 1 {
current_x += space_between_words;
}
}
Ok(())
}
fn apply_alignment(&self, x: f64, y: f64, metrics: &TextMetrics, bounds: Rect) -> (f64, f64) {
let aligned_x = match self.alignment {
Alignment::Left => x,
Alignment::Center => x + (bounds.width as f64 - metrics.width) / 2.0,
Alignment::Right => x + bounds.width as f64 - metrics.width,
Alignment::Justify => x,
};
let aligned_y = match self.vertical_alignment {
VerticalAlignment::Top => y + metrics.ascent,
VerticalAlignment::Middle => {
y + (bounds.height as f64 - metrics.height) / 2.0 + metrics.ascent
}
VerticalAlignment::Bottom => y + bounds.height as f64 - metrics.descent,
VerticalAlignment::Baseline => y,
};
(aligned_x, aligned_y)
}
fn get_wrapped_lines(&self, renderer: &TextRenderer, max_width: f64) -> Result<Vec<String>> {
let words: Vec<&str> = self.text.split_whitespace().collect();
if words.is_empty() {
return Ok(Vec::new());
}
words
.iter()
.try_fold(
(Vec::new(), String::new()),
|(mut lines, mut current_line), &word| {
let test_line = if current_line.is_empty() {
word.to_string()
} else {
format!("{} {}", current_line, word)
};
let metrics = renderer.measure_text(&test_line, &self.style)?;
if metrics.width <= max_width {
current_line = test_line;
} else {
if !current_line.is_empty() {
lines.push(current_line);
}
current_line = word.to_string();
let word_metrics = renderer.measure_text(word, &self.style)?;
if word_metrics.width > max_width {
current_line =
self.break_long_word(renderer, word, max_width, &mut lines)?;
}
}
Ok::<_, anyhow::Error>((lines, current_line))
},
)
.map(|(mut lines, current_line)| {
if !current_line.is_empty() {
lines.push(current_line);
}
lines
})
}
fn break_long_word(
&self,
renderer: &TextRenderer,
word: &str,
max_width: f64,
lines: &mut Vec<String>,
) -> Result<String> {
let mut current = String::new();
for ch in word.chars() {
let test = format!("{}{}", current, ch);
let metrics = renderer.measure_text(&test, &self.style)?;
if metrics.width > max_width && !current.is_empty() {
lines.push(current);
current = ch.to_string();
} else {
current.push(ch);
}
}
Ok(current)
}
fn apply_ellipsis(&self, renderer: &TextRenderer, max_width: f64) -> Result<String> {
let ellipsis = "...";
let ellipsis_metrics = renderer.measure_text(ellipsis, &self.style)?;
let available_width = max_width - ellipsis_metrics.width;
if available_width <= 0.0 {
return Ok(ellipsis.to_string());
}
let full_metrics = renderer.measure_text(&self.text, &self.style)?;
if full_metrics.width <= max_width {
return Ok(self.text.clone());
}
let mut truncated = String::new();
for ch in self.text.chars() {
let test = format!("{}{}", truncated, ch);
let metrics = renderer.measure_text(&test, &self.style)?;
if metrics.width > available_width {
break;
}
truncated.push(ch);
}
Ok(format!("{}{}", truncated, ellipsis))
}
}
impl Default for TextLayout {
fn default() -> Self {
Self::new()
}
}