mirror of https://github.com/helix-editor/helix
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
198 lines
6.1 KiB
Rust
198 lines
6.1 KiB
Rust
4 years ago
|
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;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|