use std::borrow::Cow;
use ratatui::layout::Alignment;
use ratatui::style::Style;
use ratatui::text::{Line, Span, Text};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::config::{ApplicationSettings, TunableValues};
use crate::util::{
replace_emojis_in_line,
replace_emojis_in_span,
replace_emojis_in_str,
space_span,
take_width,
};
pub struct TextPrinter<'a> {
text: Text<'a>,
width: usize,
base_style: Style,
alignment: Alignment,
curr_spans: Vec<Span<'a>>,
curr_width: usize,
literal: bool,
pub(super) settings: &'a ApplicationSettings,
}
impl<'a> TextPrinter<'a> {
pub fn new(width: usize, base_style: Style, settings: &'a ApplicationSettings) -> Self {
TextPrinter {
text: Text::default(),
width,
base_style,
alignment: Alignment::Left,
curr_spans: vec![],
curr_width: 0,
literal: false,
settings,
}
}
pub fn align(mut self, alignment: Alignment) -> Self {
self.alignment = alignment;
self
}
pub fn literal(mut self, literal: bool) -> Self {
self.literal = literal;
self
}
pub fn emoji_shortcodes(&self) -> bool {
self.tunables().message_shortcode_display
}
pub fn settings(&self) -> &ApplicationSettings {
self.settings
}
pub fn tunables(&self) -> &TunableValues {
&self.settings.tunables
}
pub fn width(&self) -> usize {
self.width
}
pub fn sub(&self, indent: usize) -> Self {
TextPrinter {
text: Text::default(),
width: self.width.saturating_sub(indent),
base_style: self.base_style,
alignment: self.alignment,
curr_spans: vec![],
curr_width: 0,
literal: self.literal,
settings: self.settings,
}
}
fn remaining(&self) -> usize {
self.width.saturating_sub(self.curr_width)
}
pub fn commit(&mut self) {
if self.curr_width > 0 {
self.push_break();
}
}
fn push(&mut self) {
self.curr_width = 0;
self.text.lines.push(Line::from(std::mem::take(&mut self.curr_spans)));
}
pub fn push_break(&mut self) {
if self.curr_width == 0 && self.text.lines.is_empty() {
return;
}
let remaining = self.remaining();
if remaining > 0 {
match self.alignment {
Alignment::Left => {
let tspan = space_span(remaining, self.base_style);
self.curr_spans.push(tspan);
},
Alignment::Center => {
let trailing = remaining / 2;
let leading = remaining - trailing;
let tspan = space_span(trailing, self.base_style);
let lspan = space_span(leading, self.base_style);
self.curr_spans.push(tspan);
self.curr_spans.insert(0, lspan);
},
Alignment::Right => {
let lspan = space_span(remaining, self.base_style);
self.curr_spans.insert(0, lspan);
},
}
}
self.push();
}
fn push_str_wrapped<T>(&mut self, s: T, style: Style)
where
T: Into<Cow<'a, str>>,
{
let style = self.base_style.patch(style);
let mut cow = s.into();
loop {
let sw = UnicodeWidthStr::width(cow.as_ref());
if self.curr_width + sw <= self.width {
self.curr_spans.push(Span::styled(cow, style));
self.curr_width += sw;
break;
}
let ((s0, w), s1) = take_width(cow, self.remaining());
cow = s1;
self.curr_spans.push(Span::styled(s0, style));
self.curr_width += w;
self.commit();
}
if self.curr_width == self.width {
self.push();
}
}
pub fn push_span_nobreak(&mut self, mut span: Span<'a>) {
if self.emoji_shortcodes() {
replace_emojis_in_span(&mut span);
}
let sw = UnicodeWidthStr::width(span.content.as_ref());
if self.curr_width + sw > self.width {
self.commit();
}
self.curr_spans.push(span);
self.curr_width += sw;
}
pub fn push_str(&mut self, s: &'a str, style: Style) {
let style = self.base_style.patch(style);
if self.width == 0 {
return;
}
let tabstop = self.settings().tunables.tabstop;
for mut word in UnicodeSegmentation::split_word_bounds(s) {
if let "\n" | "\r\n" = word {
if self.literal {
self.commit();
continue;
}
word = " ";
}
if !self.literal && self.curr_width == 0 && word.chars().all(char::is_whitespace) {
continue;
}
let mut cow = if self.emoji_shortcodes() {
Cow::Owned(replace_emojis_in_str(word))
} else {
Cow::Borrowed(word)
};
if cow == "\t" {
let tablen = tabstop - (self.curr_width % tabstop);
cow = Cow::Owned(" ".repeat(tablen));
}
let sw = UnicodeWidthStr::width(cow.as_ref());
if sw > self.width {
self.push_str_wrapped(cow, style);
continue;
}
if self.curr_width + sw > self.width {
self.commit();
if !self.literal && cow.chars().all(char::is_whitespace) {
continue;
}
}
let span = Span::styled(cow, style);
self.curr_spans.push(span);
self.curr_width += sw;
}
if self.curr_width == self.width {
self.push();
}
}
pub fn push_line(&mut self, mut line: Line<'a>) {
self.commit();
if self.emoji_shortcodes() {
replace_emojis_in_line(&mut line);
}
self.text.lines.push(line);
}
pub fn push_text(&mut self, mut text: Text<'a>) {
self.commit();
if self.emoji_shortcodes() {
for line in &mut text.lines {
replace_emojis_in_line(line);
}
}
self.text.lines.extend(text.lines);
}
pub fn finish(mut self) -> Text<'a> {
self.commit();
self.text
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::tests::mock_settings;
#[test]
fn test_push_nobreak() {
let settings = mock_settings();
let mut printer = TextPrinter::new(5, Style::default(), &settings);
printer.push_span_nobreak("hello world".into());
let text = printer.finish();
assert_eq!(text.lines.len(), 1);
assert_eq!(text.lines[0].spans.len(), 1);
assert_eq!(text.lines[0].spans[0].content, "hello world");
}
}