diff options
Diffstat (limited to 'helix-tui/src/widgets/paragraph.rs')
-rw-r--r-- | helix-tui/src/widgets/paragraph.rs | 197 |
1 files changed, 197 insertions, 0 deletions
diff --git a/helix-tui/src/widgets/paragraph.rs b/helix-tui/src/widgets/paragraph.rs new file mode 100644 index 00000000..ecce8581 --- /dev/null +++ b/helix-tui/src/widgets/paragraph.rs @@ -0,0 +1,197 @@ +use crate::{ + buffer::Buffer, + layout::{Alignment, Rect}, + style::Style, + text::{StyledGrapheme, Text}, + widgets::{ + reflow::{LineComposer, LineTruncator, WordWrapper}, + Block, 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, + } +} + +/// A widget to display some text. +/// +/// # Examples +/// +/// ``` +/// # use helix_tui::text::{Text, Spans, Span}; +/// # use helix_tui::widgets::{Block, Borders, Paragraph, Wrap}; +/// # use helix_tui::style::{Style, Color, Modifier}; +/// # use helix_tui::layout::{Alignment}; +/// let text = vec![ +/// Spans::from(vec![ +/// Span::raw("First"), +/// Span::styled("line",Style::default().add_modifier(Modifier::ITALIC)), +/// Span::raw("."), +/// ]), +/// Spans::from(Span::styled("Second line", Style::default().fg(Color::Red))), +/// ]; +/// Paragraph::new(text) +/// .block(Block::default().title("Paragraph").borders(Borders::ALL)) +/// .style(Style::default().fg(Color::White).bg(Color::Black)) +/// .alignment(Alignment::Center) +/// .wrap(Wrap { trim: true }); +/// ``` +#[derive(Debug, Clone)] +pub struct Paragraph<'a> { + /// A block to wrap the widget in + block: Option<Block<'a>>, + /// Widget style + style: Style, + /// How to wrap the text + wrap: Option<Wrap>, + /// The text to display + text: Text<'a>, + /// Scroll + scroll: (u16, u16), + /// Alignment of the text + alignment: Alignment, +} + +/// Describes how to wrap text across lines. +/// +/// ## Examples +/// +/// ``` +/// # use helix_tui::widgets::{Paragraph, Wrap}; +/// # use helix_tui::text::Text; +/// let bullet_points = Text::from(r#"Some indented points: +/// - First thing goes here and is long so that it wraps +/// - Here is another point that is long enough to wrap"#); +/// +/// // With leading spaces trimmed (window width of 30 chars): +/// Paragraph::new(bullet_points.clone()).wrap(Wrap { trim: true }); +/// // Some indented points: +/// // - First thing goes here and is +/// // long so that it wraps +/// // - Here is another point that +/// // is long enough to wrap +/// +/// // But without trimming, indentation is preserved: +/// Paragraph::new(bullet_points).wrap(Wrap { trim: false }); +/// // Some indented points: +/// // - First thing goes here +/// // and is long so that it wraps +/// // - Here is another point +/// // that is long enough to wrap +/// ``` +#[derive(Debug, Clone, Copy)] +pub struct Wrap { + /// Should leading whitespace be trimmed + pub trim: bool, +} + +impl<'a> Paragraph<'a> { + pub fn new<T>(text: T) -> Paragraph<'a> + where + T: Into<Text<'a>>, + { + Paragraph { + block: None, + style: Default::default(), + wrap: None, + text: text.into(), + scroll: (0, 0), + alignment: Alignment::Left, + } + } + + pub fn block(mut self, block: Block<'a>) -> Paragraph<'a> { + self.block = Some(block); + self + } + + pub fn style(mut self, style: Style) -> Paragraph<'a> { + self.style = style; + self + } + + pub fn wrap(mut self, wrap: Wrap) -> Paragraph<'a> { + self.wrap = Some(wrap); + self + } + + pub fn scroll(mut self, offset: (u16, u16)) -> Paragraph<'a> { + self.scroll = offset; + self + } + + pub fn alignment(mut self, alignment: Alignment) -> Paragraph<'a> { + self.alignment = alignment; + self + } +} + +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.take() { + Some(b) => { + let inner_area = b.inner(area); + b.render(area, buf); + inner_area + } + None => area, + }; + + 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)) + // Required given the way composers work but might be refactored out if we change + // composers to operate on lines instead of a stream of graphemes. + .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; + while let Some((current_line, current_line_width)) = line_composer.next_line() { + if y >= self.scroll.0 { + 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() { + // If the symbol is empty, the last char which rendered last time will + // leave on the line. It's a quick fix. + " " + } else { + symbol + }) + .set_style(*style); + x += symbol.width() as u16; + } + } + y += 1; + if y >= text_area.height + self.scroll.0 { + break; + } + } + } +} |