use crate::{
buffer::Buffer,
layout::{Alignment, Direction, Margin, Rect},
style::Style,
text::{StyledGrapheme, Text},
widgets::{
reflow::{LineComposer, LineTruncator, WordWrapper},
Block, Borders, Scrollbar, ScrollbarOrientation, Widget,
},
};
use std::iter;
use unicode_width::UnicodeWidthStr;
fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 {
match alignment {
Alignment::Center => (text_area_width / 2).saturating_sub(line_width / 2),
Alignment::Right => text_area_width.saturating_sub(line_width),
Alignment::Left => 0,
}
}
#[derive(Debug, Clone)]
pub struct Paragraph<'a> {
block: Option<Block<'a>>,
scrollbar: Option<Scrollbar<'a>>,
scrollbar_direction: Option<Direction>,
margin: Margin,
style: Style,
wrap: Option<Wrap>,
text: Text<'a>,
scroll: (u16, u16),
alignment: Alignment,
content_height: u16,
content_width: u16,
text_was_updated: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct Wrap {
pub trim: bool,
}
impl<'a> Paragraph<'a> {
pub fn new<T>(text: T) -> Paragraph<'a>
where
T: Into<Text<'a>>,
{
Paragraph {
block: None,
scrollbar: None,
scrollbar_direction: None,
margin: Margin::default(),
style: Default::default(),
wrap: None,
text: text.into(),
scroll: (0, 0),
alignment: Alignment::Left,
content_height: 0,
content_width: 0,
text_was_updated: true,
}
}
pub fn text<T>(&mut self, text: T)
where
T: Into<Text<'a>>,
{
self.text = text.into();
self.text_was_updated = true;
}
pub fn block(&mut self, block: Block<'a>) {
self.block = Some(block);
}
pub fn scrollbar(&mut self, scrollbar: Scrollbar<'a>) {
self.scrollbar_direction = match scrollbar.show_orientation() {
ScrollbarOrientation::VerticalRight | ScrollbarOrientation::VerticalLeft => {
Some(Direction::Vertical)
}
_ => Some(Direction::Horizontal),
};
if self.block.is_none() {
self.block = Some(Block::default().borders(Borders::NONE));
}
self.scrollbar = Some(scrollbar);
}
pub fn text_style(&mut self, style: Style) {
self.text.patch_style(style);
}
pub fn style(&mut self, style: Style) {
self.style = style;
}
pub fn wrap(&mut self, wrap: Wrap) {
self.wrap = Some(wrap);
}
pub fn margin(&mut self, margin: u16) {
self.margin = Margin {
horizontal: margin,
vertical: margin,
};
}
pub fn horizontal_margin(&mut self, horizontal: u16) {
self.margin.horizontal = horizontal;
}
pub fn vertical_margin(&mut self, vertical: u16) {
self.margin.vertical = vertical;
}
pub fn scroll(&mut self, offset: (u16, u16)) {
self.scroll = offset;
}
pub fn alignment(&mut self, alignment: Alignment) {
self.alignment = alignment;
}
pub fn content_height(&self) -> Option<u16> {
match self.scrollbar_direction.as_ref() {
Some(Direction::Vertical) => Some(self.content_height),
_ => None,
}
}
pub fn content_width(&self) -> Option<u16> {
match self.scrollbar_direction.as_ref() {
Some(Direction::Horizontal) => Some(self.content_width),
_ => None,
}
}
}
impl<'a> Widget for Paragraph<'a> {
fn render(&mut self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
let text_area = match self.block.as_mut() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
inner_area
}
None => area,
};
let text_area = text_area.inner(&self.margin);
if !self.text_was_updated && self.scrollbar.is_some() {
self.scroll = (
self.scroll
.0
.min(self.content_height.saturating_sub(text_area.height)),
self.scroll
.1
.min(self.content_width.saturating_sub(text_area.width)),
)
}
if text_area.height < 1 {
return;
}
let style = self.style;
let mut styled = self.text.lines.iter().flat_map(|spans| {
spans
.0
.iter()
.flat_map(|span| span.styled_graphemes(style))
.chain(iter::once(StyledGrapheme {
symbol: "\n",
style: self.style,
}))
});
let mut line_composer: Box<dyn LineComposer> = if let Some(Wrap { trim }) = self.wrap {
Box::new(WordWrapper::new(&mut styled, text_area.width, trim))
} else {
let mut line_composer = Box::new(LineTruncator::new(&mut styled, text_area.width));
if let Alignment::Left = self.alignment {
line_composer.set_horizontal_offset(self.scroll.1);
}
line_composer
};
let mut y = 0;
let mut max_line_width = 0;
let height = text_area.height + self.scroll.0;
while let Some((current_line, current_line_width)) = line_composer.next_line() {
if y >= self.scroll.0 && y < height {
let mut x = get_line_offset(current_line_width, text_area.width, self.alignment);
for StyledGrapheme { symbol, style } in current_line {
buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll.0)
.set_symbol(if symbol.is_empty() {
" "
} else {
symbol
})
.set_style(*style);
x += symbol.width() as u16;
}
}
y += 1;
if current_line_width > max_line_width && !self.text_was_updated {
max_line_width = current_line_width;
}
if y >= height && !self.text_was_updated {
break;
}
}
if self.text_was_updated {
self.content_height = y;
self.content_width = max_line_width;
self.text_was_updated = false;
}
if let (Some(scrollbar), Some(dir)) = (self.scrollbar.as_mut(), &self.scrollbar_direction) {
match dir {
Direction::Horizontal => {
scrollbar.offset(self.scroll.1);
scrollbar.content_length(self.content_width);
}
Direction::Vertical => {
scrollbar.offset(self.scroll.0);
scrollbar.content_length(self.content_height);
}
}
scrollbar.render(area, buf);
}
}
}