aboutsummaryrefslogtreecommitdiffstats
path: root/src/tui/widgets
diff options
context:
space:
mode:
Diffstat (limited to 'src/tui/widgets')
-rw-r--r--src/tui/widgets/block.rs562
-rw-r--r--src/tui/widgets/mod.rs159
-rw-r--r--src/tui/widgets/paragraph.rs194
-rw-r--r--src/tui/widgets/reflow.rs537
4 files changed, 0 insertions, 1452 deletions
diff --git a/src/tui/widgets/block.rs b/src/tui/widgets/block.rs
deleted file mode 100644
index eb59f9c3..00000000
--- a/src/tui/widgets/block.rs
+++ /dev/null
@@ -1,562 +0,0 @@
-use crate::tui::{
- buffer::Buffer,
- layout::{Alignment, Rect},
- style::Style,
- symbols::line,
- text::Spans,
- widgets::{Borders, Widget},
-};
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum BorderType {
- Plain,
- Rounded,
- Double,
- Thick,
-}
-
-impl BorderType {
- pub fn line_symbols(border_type: BorderType) -> line::Set {
- match border_type {
- BorderType::Plain => line::NORMAL,
- BorderType::Rounded => line::ROUNDED,
- BorderType::Double => line::DOUBLE,
- BorderType::Thick => line::THICK,
- }
- }
-}
-
-/// Base widget to be used with all upper level ones. It may be used to display a box border around
-/// the widget and/or add a title.
-///
-/// # Examples
-///
-/// ```
-/// # use tui::widgets::{Block, BorderType, Borders};
-/// # use tui::style::{Style, Color};
-/// Block::default()
-/// .title("Block")
-/// .borders(Borders::LEFT | Borders::RIGHT)
-/// .border_style(Style::default().fg(Color::White))
-/// .border_type(BorderType::Rounded)
-/// .style(Style::default().bg(Color::Black));
-/// ```
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct Block<'a> {
- /// Optional title place on the upper left of the block
- title: Option<Spans<'a>>,
- /// Title alignment. The default is top left of the block, but one can choose to place
- /// title in the top middle, or top right of the block
- title_alignment: Alignment,
- /// Visible borders
- borders: Borders,
- /// Border style
- border_style: Style,
- /// Type of the border. The default is plain lines but one can choose to have rounded corners
- /// or doubled lines instead.
- border_type: BorderType,
- /// Widget style
- style: Style,
-}
-
-impl<'a> Default for Block<'a> {
- fn default() -> Block<'a> {
- Block {
- title: None,
- title_alignment: Alignment::Left,
- borders: Borders::NONE,
- border_style: Style::default(),
- border_type: BorderType::Plain,
- style: Style::default(),
- }
- }
-}
-
-impl<'a> Block<'a> {
- pub fn title<T>(mut self, title: T) -> Block<'a>
- where
- T: Into<Spans<'a>>,
- {
- self.title = Some(title.into());
- self
- }
-
- pub fn title_alignment(mut self, alignment: Alignment) -> Block<'a> {
- self.title_alignment = alignment;
- self
- }
-
- pub fn border_style(mut self, style: Style) -> Block<'a> {
- self.border_style = style;
- self
- }
-
- pub fn style(mut self, style: Style) -> Block<'a> {
- self.style = style;
- self
- }
-
- pub fn borders(mut self, flag: Borders) -> Block<'a> {
- self.borders = flag;
- self
- }
-
- pub fn border_type(mut self, border_type: BorderType) -> Block<'a> {
- self.border_type = border_type;
- self
- }
-
- /// Compute the inner area of a block based on its border visibility rules.
- pub fn inner(&self, area: Rect) -> Rect {
- let mut inner = area;
- if self.borders.intersects(Borders::LEFT) {
- inner.x = inner.x.saturating_add(1).min(inner.right());
- inner.width = inner.width.saturating_sub(1);
- }
- if self.borders.intersects(Borders::TOP) || self.title.is_some() {
- inner.y = inner.y.saturating_add(1).min(inner.bottom());
- inner.height = inner.height.saturating_sub(1);
- }
- if self.borders.intersects(Borders::RIGHT) {
- inner.width = inner.width.saturating_sub(1);
- }
- if self.borders.intersects(Borders::BOTTOM) {
- inner.height = inner.height.saturating_sub(1);
- }
- inner
- }
-}
-
-impl<'a> Widget for Block<'a> {
- fn render(self, area: Rect, buf: &mut Buffer) {
- if area.area() == 0 {
- return;
- }
- buf.set_style(area, self.style);
- let symbols = BorderType::line_symbols(self.border_type);
-
- // Sides
- if self.borders.intersects(Borders::LEFT) {
- for y in area.top()..area.bottom() {
- buf.get_mut(area.left(), y)
- .set_symbol(symbols.vertical)
- .set_style(self.border_style);
- }
- }
- if self.borders.intersects(Borders::TOP) {
- for x in area.left()..area.right() {
- buf.get_mut(x, area.top())
- .set_symbol(symbols.horizontal)
- .set_style(self.border_style);
- }
- }
- if self.borders.intersects(Borders::RIGHT) {
- let x = area.right() - 1;
- for y in area.top()..area.bottom() {
- buf.get_mut(x, y)
- .set_symbol(symbols.vertical)
- .set_style(self.border_style);
- }
- }
- if self.borders.intersects(Borders::BOTTOM) {
- let y = area.bottom() - 1;
- for x in area.left()..area.right() {
- buf.get_mut(x, y)
- .set_symbol(symbols.horizontal)
- .set_style(self.border_style);
- }
- }
-
- // Corners
- if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
- buf.get_mut(area.right() - 1, area.bottom() - 1)
- .set_symbol(symbols.bottom_right)
- .set_style(self.border_style);
- }
- if self.borders.contains(Borders::RIGHT | Borders::TOP) {
- buf.get_mut(area.right() - 1, area.top())
- .set_symbol(symbols.top_right)
- .set_style(self.border_style);
- }
- if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
- buf.get_mut(area.left(), area.bottom() - 1)
- .set_symbol(symbols.bottom_left)
- .set_style(self.border_style);
- }
- if self.borders.contains(Borders::LEFT | Borders::TOP) {
- buf.get_mut(area.left(), area.top())
- .set_symbol(symbols.top_left)
- .set_style(self.border_style);
- }
-
- // Title
- if let Some(title) = self.title {
- let left_border_dx = if self.borders.intersects(Borders::LEFT) {
- 1
- } else {
- 0
- };
-
- let right_border_dx = if self.borders.intersects(Borders::RIGHT) {
- 1
- } else {
- 0
- };
-
- let title_area_width = area
- .width
- .saturating_sub(left_border_dx)
- .saturating_sub(right_border_dx);
-
- let title_dx = match self.title_alignment {
- Alignment::Left => left_border_dx,
- Alignment::Center => area.width.saturating_sub(title.width() as u16) / 2,
- Alignment::Right => area
- .width
- .saturating_sub(title.width() as u16)
- .saturating_sub(right_border_dx),
- };
-
- let title_x = area.left() + title_dx;
- let title_y = area.top();
-
- buf.set_spans(title_x, title_y, &title, title_area_width);
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::tui::layout::Rect;
-
- #[test]
- #[allow(clippy::too_many_lines)]
- fn inner_takes_into_account_the_borders() {
- // No borders
- assert_eq!(
- Block::default().inner(Rect::default()),
- Rect {
- x: 0,
- y: 0,
- width: 0,
- height: 0
- },
- "no borders, width=0, height=0"
- );
- assert_eq!(
- Block::default().inner(Rect {
- x: 0,
- y: 0,
- width: 1,
- height: 1
- }),
- Rect {
- x: 0,
- y: 0,
- width: 1,
- height: 1
- },
- "no borders, width=1, height=1"
- );
-
- // Left border
- assert_eq!(
- Block::default().borders(Borders::LEFT).inner(Rect {
- x: 0,
- y: 0,
- width: 0,
- height: 1
- }),
- Rect {
- x: 0,
- y: 0,
- width: 0,
- height: 1
- },
- "left, width=0"
- );
- assert_eq!(
- Block::default().borders(Borders::LEFT).inner(Rect {
- x: 0,
- y: 0,
- width: 1,
- height: 1
- }),
- Rect {
- x: 1,
- y: 0,
- width: 0,
- height: 1
- },
- "left, width=1"
- );
- assert_eq!(
- Block::default().borders(Borders::LEFT).inner(Rect {
- x: 0,
- y: 0,
- width: 2,
- height: 1
- }),
- Rect {
- x: 1,
- y: 0,
- width: 1,
- height: 1
- },
- "left, width=2"
- );
-
- // Top border
- assert_eq!(
- Block::default().borders(Borders::TOP).inner(Rect {
- x: 0,
- y: 0,
- width: 1,
- height: 0
- }),
- Rect {
- x: 0,
- y: 0,
- width: 1,
- height: 0
- },
- "top, height=0"
- );
- assert_eq!(
- Block::default().borders(Borders::TOP).inner(Rect {
- x: 0,
- y: 0,
- width: 1,
- height: 1
- }),
- Rect {
- x: 0,
- y: 1,
- width: 1,
- height: 0
- },
- "top, height=1"
- );
- assert_eq!(
- Block::default().borders(Borders::TOP).inner(Rect {
- x: 0,
- y: 0,
- width: 1,
- height: 2
- }),
- Rect {
- x: 0,
- y: 1,
- width: 1,
- height: 1
- },
- "top, height=2"
- );
-
- // Right border
- assert_eq!(
- Block::default().borders(Borders::RIGHT).inner(Rect {
- x: 0,
- y: 0,
- width: 0,
- height: 1
- }),
- Rect {
- x: 0,
- y: 0,
- width: 0,
- height: 1
- },
- "right, width=0"
- );
- assert_eq!(
- Block::default().borders(Borders::RIGHT).inner(Rect {
- x: 0,
- y: 0,
- width: 1,
- height: 1
- }),
- Rect {
- x: 0,
- y: 0,
- width: 0,
- height: 1
- },
- "right, width=1"
- );
- assert_eq!(
- Block::default().borders(Borders::RIGHT).inner(Rect {
- x: 0,
- y: 0,
- width: 2,
- height: 1
- }),
- Rect {
- x: 0,
- y: 0,
- width: 1,
- height: 1
- },
- "right, width=2"
- );
-
- // Bottom border
- assert_eq!(
- Block::default().borders(Borders::BOTTOM).inner(Rect {
- x: 0,
- y: 0,
- width: 1,
- height: 0
- }),
- Rect {
- x: 0,
- y: 0,
- width: 1,
- height: 0
- },
- "bottom, height=0"
- );
- assert_eq!(
- Block::default().borders(Borders::BOTTOM).inner(Rect {
- x: 0,
- y: 0,
- width: 1,
- height: 1
- }),
- Rect {
- x: 0,
- y: 0,
- width: 1,
- height: 0
- },
- "bottom, height=1"
- );
- assert_eq!(
- Block::default().borders(Borders::BOTTOM).inner(Rect {
- x: 0,
- y: 0,
- width: 1,
- height: 2
- }),
- Rect {
- x: 0,
- y: 0,
- width: 1,
- height: 1
- },
- "bottom, height=2"
- );
-
- // All borders
- assert_eq!(
- Block::default()
- .borders(Borders::ALL)
- .inner(Rect::default()),
- Rect {
- x: 0,
- y: 0,
- width: 0,
- height: 0
- },
- "all borders, width=0, height=0"
- );
- assert_eq!(
- Block::default().borders(Borders::ALL).inner(Rect {
- x: 0,
- y: 0,
- width: 1,
- height: 1
- }),
- Rect {
- x: 1,
- y: 1,
- width: 0,
- height: 0,
- },
- "all borders, width=1, height=1"
- );
- assert_eq!(
- Block::default().borders(Borders::ALL).inner(Rect {
- x: 0,
- y: 0,
- width: 2,
- height: 2,
- }),
- Rect {
- x: 1,
- y: 1,
- width: 0,
- height: 0,
- },
- "all borders, width=2, height=2"
- );
- assert_eq!(
- Block::default().borders(Borders::ALL).inner(Rect {
- x: 0,
- y: 0,
- width: 3,
- height: 3,
- }),
- Rect {
- x: 1,
- y: 1,
- width: 1,
- height: 1,
- },
- "all borders, width=3, height=3"
- );
- }
-
- #[test]
- fn inner_takes_into_account_the_title() {
- assert_eq!(
- Block::default().title("Test").inner(Rect {
- x: 0,
- y: 0,
- width: 0,
- height: 1,
- }),
- Rect {
- x: 0,
- y: 1,
- width: 0,
- height: 0,
- },
- );
- assert_eq!(
- Block::default()
- .title("Test")
- .title_alignment(Alignment::Center)
- .inner(Rect {
- x: 0,
- y: 0,
- width: 0,
- height: 1,
- }),
- Rect {
- x: 0,
- y: 1,
- width: 0,
- height: 0,
- },
- );
- assert_eq!(
- Block::default()
- .title("Test")
- .title_alignment(Alignment::Right)
- .inner(Rect {
- x: 0,
- y: 0,
- width: 0,
- height: 1,
- }),
- Rect {
- x: 0,
- y: 1,
- width: 0,
- height: 0,
- },
- );
- }
-}
diff --git a/src/tui/widgets/mod.rs b/src/tui/widgets/mod.rs
deleted file mode 100644
index 635185e5..00000000
--- a/src/tui/widgets/mod.rs
+++ /dev/null
@@ -1,159 +0,0 @@
-//! `widgets` is a collection of types that implement [`Widget`] or [`StatefulWidget`] or both.
-//!
-//! All widgets are implemented using the builder pattern and are consumable objects. They are not
-//! meant to be stored but used as *commands* to draw common figures in the UI.
-//!
-//! The available widgets are:
-//! - [`Block`]
-//! - [`Paragraph`]
-
-mod block;
-mod paragraph;
-mod reflow;
-
-pub use self::block::{Block, BorderType};
-pub use self::paragraph::{Paragraph, Wrap};
-
-use crate::tui::{buffer::Buffer, layout::Rect};
-use bitflags::bitflags;
-
-bitflags! {
- /// Bitflags that can be composed to set the visible borders essentially on the block widget.
- pub struct Borders: u32 {
- /// Show no border (default)
- const NONE = 0b0000_0001;
- /// Show the top border
- const TOP = 0b0000_0010;
- /// Show the right border
- const RIGHT = 0b0000_0100;
- /// Show the bottom border
- const BOTTOM = 0b000_1000;
- /// Show the left border
- const LEFT = 0b0001_0000;
- /// Show all borders
- const ALL = Self::TOP.bits | Self::RIGHT.bits | Self::BOTTOM.bits | Self::LEFT.bits;
- }
-}
-
-/// Base requirements for a Widget
-pub trait Widget {
- /// Draws the current state of the widget in the given buffer. That is the only method required
- /// to implement a custom widget.
- fn render(self, area: Rect, buf: &mut Buffer);
-}
-
-/// A `StatefulWidget` is a widget that can take advantage of some local state to remember things
-/// between two draw calls.
-///
-/// Most widgets can be drawn directly based on the input parameters. However, some features may
-/// require some kind of associated state to be implemented.
-///
-/// For example, the [`List`] widget can highlight the item currently selected. This can be
-/// translated in an offset, which is the number of elements to skip in order to have the selected
-/// item within the viewport currently allocated to this widget. The widget can therefore only
-/// provide the following behavior: whenever the selected item is out of the viewport scroll to a
-/// predefined position (making the selected item the last viewable item or the one in the middle
-/// for example). Nonetheless, if the widget has access to the last computed offset then it can
-/// implement a natural scrolling experience where the last offset is reused until the selected
-/// item is out of the viewport.
-///
-/// ## Examples
-///
-/// ```rust,no_run
-/// # use std::io;
-/// # use tui::Terminal;
-/// # use tui::backend::{Backend, TestBackend};
-/// # use tui::widgets::{Widget, List, ListItem, ListState};
-///
-/// // Let's say we have some events to display.
-/// struct Events {
-/// // `items` is the state managed by your application.
-/// items: Vec<String>,
-/// // `state` is the state that can be modified by the UI. It stores the index of the selected
-/// // item as well as the offset computed during the previous draw call (used to implement
-/// // natural scrolling).
-/// state: ListState
-/// }
-///
-/// impl Events {
-/// fn new(items: Vec<String>) -> Events {
-/// Events {
-/// items,
-/// state: ListState::default(),
-/// }
-/// }
-///
-/// pub fn set_items(&mut self, items: Vec<String>) {
-/// self.items = items;
-/// // We reset the state as the associated items have changed. This effectively reset
-/// // the selection as well as the stored offset.
-/// self.state = ListState::default();
-/// }
-///
-/// // Select the next item. This will not be reflected until the widget is drawn in the
-/// // `Terminal::draw` callback using `Frame::render_stateful_widget`.
-/// pub fn next(&mut self) {
-/// let i = match self.state.selected() {
-/// Some(i) => {
-/// if i >= self.items.len() - 1 {
-/// 0
-/// } else {
-/// i + 1
-/// }
-/// }
-/// None => 0,
-/// };
-/// self.state.select(Some(i));
-/// }
-///
-/// // Select the previous item. This will not be reflected until the widget is drawn in the
-/// // `Terminal::draw` callback using `Frame::render_stateful_widget`.
-/// pub fn previous(&mut self) {
-/// let i = match self.state.selected() {
-/// Some(i) => {
-/// if i == 0 {
-/// self.items.len() - 1
-/// } else {
-/// i - 1
-/// }
-/// }
-/// None => 0,
-/// };
-/// self.state.select(Some(i));
-/// }
-///
-/// // Unselect the currently selected item if any. The implementation of `ListState` makes
-/// // sure that the stored offset is also reset.
-/// pub fn unselect(&mut self) {
-/// self.state.select(None);
-/// }
-/// }
-///
-/// # let backend = TestBackend::new(5, 5);
-/// # let mut terminal = Terminal::new(backend).unwrap();
-///
-/// let mut events = Events::new(vec![
-/// String::from("Item 1"),
-/// String::from("Item 2")
-/// ]);
-///
-/// loop {
-/// terminal.draw(|f| {
-/// // The items managed by the application are transformed to something
-/// // that is understood by tui.
-/// let items: Vec<ListItem>= events.items.iter().map(|i| ListItem::new(i.as_ref())).collect();
-/// // The `List` widget is then built with those items.
-/// let list = List::new(items);
-/// // Finally the widget is rendered using the associated state. `events.state` is
-/// // effectively the only thing that we will "remember" from this draw call.
-/// f.render_stateful_widget(list, f.size(), &mut events.state);
-/// });
-///
-/// // In response to some input events or an external http request or whatever:
-/// events.next();
-/// }
-/// ```
-pub trait StatefulWidget {
- type State;
- fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
-}
diff --git a/src/tui/widgets/paragraph.rs b/src/tui/widgets/paragraph.rs
deleted file mode 100644
index a5036148..00000000
--- a/src/tui/widgets/paragraph.rs
+++ /dev/null
@@ -1,194 +0,0 @@
-use crate::tui::{
- 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 tui::text::{Text, Spans, Span};
-/// # use tui::widgets::{Block, Borders, Paragraph, Wrap};
-/// # use tui::style::{Style, Color, Modifier};
-/// # use 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 tui::widgets::{Paragraph, Wrap};
-/// # use 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: Style::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 = self.block.take().map_or(area, |b| {
- let inner_area = b.inner(area);
- b.render(area, buf);
- inner_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 self.alignment == Alignment::Left {
- 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;
- }
- }
- }
-}
diff --git a/src/tui/widgets/reflow.rs b/src/tui/widgets/reflow.rs
deleted file mode 100644
index d174932b..00000000
--- a/src/tui/widgets/reflow.rs
+++ /dev/null
@@ -1,537 +0,0 @@
-use crate::tui::text::StyledGrapheme;
-use unicode_segmentation::UnicodeSegmentation;
-use unicode_width::UnicodeWidthStr;
-
-const NBSP: &str = "\u{00a0}";
-
-/// A state machine to pack styled symbols into lines.
-/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming
-/// iterators for that).
-pub trait LineComposer<'a> {
- fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)>;
-}
-
-/// A state machine that wraps lines on word boundaries.
-pub struct WordWrapper<'a, 'b> {
- symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
- max_line_width: u16,
- current_line: Vec<StyledGrapheme<'a>>,
- next_line: Vec<StyledGrapheme<'a>>,
- /// Removes the leading whitespace from lines
- trim: bool,
-}
-
-impl<'a, 'b> WordWrapper<'a, 'b> {
- pub fn new(
- symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
- max_line_width: u16,
- trim: bool,
- ) -> WordWrapper<'a, 'b> {
- WordWrapper {
- symbols,
- max_line_width,
- current_line: vec![],
- next_line: vec![],
- trim,
- }
- }
-}
-
-impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
- fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> {
- if self.max_line_width == 0 {
- return None;
- }
- std::mem::swap(&mut self.current_line, &mut self.next_line);
- self.next_line.truncate(0);
-
- let mut current_line_width = self
- .current_line
- .iter()
- .map(|StyledGrapheme { symbol, .. }| symbol.width() as u16)
- .sum();
-
- let mut symbols_to_last_word_end: usize = 0;
- let mut width_to_last_word_end: u16 = 0;
- let mut prev_whitespace = false;
- let mut symbols_exhausted = true;
- for StyledGrapheme { symbol, style } in &mut self.symbols {
- symbols_exhausted = false;
- let symbol_whitespace = symbol.chars().all(&char::is_whitespace) && symbol != NBSP;
-
- // Ignore characters wider that the total max width.
- if symbol.width() as u16 > self.max_line_width
- // Skip leading whitespace when trim is enabled.
- || self.trim && symbol_whitespace && symbol != "\n" && current_line_width == 0
- {
- continue;
- }
-
- // Break on newline and discard it.
- if symbol == "\n" {
- if prev_whitespace {
- current_line_width = width_to_last_word_end;
- self.current_line.truncate(symbols_to_last_word_end);
- }
- break;
- }
-
- // Mark the previous symbol as word end.
- if symbol_whitespace && !prev_whitespace {
- symbols_to_last_word_end = self.current_line.len();
- width_to_last_word_end = current_line_width;
- }
-
- self.current_line.push(StyledGrapheme { symbol, style });
- current_line_width += symbol.width() as u16;
-
- if current_line_width > self.max_line_width {
- // If there was no word break in the text, wrap at the end of the line.
- let (truncate_at, truncated_width) = if symbols_to_last_word_end == 0 {
- (self.current_line.len() - 1, self.max_line_width)
- } else {
- (symbols_to_last_word_end, width_to_last_word_end)
- };
-
- // Push the remainder to the next line but strip leading whitespace:
- {
- let remainder = &self.current_line[truncate_at..];
- if let Some(remainder_nonwhite) =
- remainder.iter().position(|StyledGrapheme { symbol, .. }| {
- !symbol.chars().all(&char::is_whitespace)
- })
- {
- self.next_line
- .extend_from_slice(&remainder[remainder_nonwhite..]);
- }
- }
- self.current_line.truncate(truncate_at);
- current_line_width = truncated_width;
- break;
- }
-
- prev_whitespace = symbol_whitespace;
- }
-
- // Even if the iterator is exhausted, pass the previous remainder.
- if symbols_exhausted && self.current_line.is_empty() {
- None
- } else {
- Some((&self.current_line[..], current_line_width))
- }
- }
-}
-
-/// A state machine that truncates overhanging lines.
-pub struct LineTruncator<'a, 'b> {
- symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
- max_line_width: u16,
- current_line: Vec<StyledGrapheme<'a>>,
- /// Record the offet to skip render
- horizontal_offset: u16,
-}
-
-impl<'a, 'b> LineTruncator<'a, 'b> {
- pub fn new(
- symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
- max_line_width: u16,
- ) -> LineTruncator<'a, 'b> {
- LineTruncator {
- symbols,
- max_line_width,
- horizontal_offset: 0,
- current_line: vec![],
- }
- }
-
- pub fn set_horizontal_offset(&mut self, horizontal_offset: u16) {
- self.horizontal_offset = horizontal_offset;
- }
-}
-
-impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> {
- fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> {
- if self.max_line_width == 0 {
- return None;
- }
-
- self.current_line.truncate(0);
- let mut current_line_width = 0;
-
- let mut skip_rest = false;
- let mut symbols_exhausted = true;
- let mut horizontal_offset = self.horizontal_offset as usize;
- for StyledGrapheme { symbol, style } in &mut self.symbols {
- symbols_exhausted = false;
-
- // Ignore characters wider that the total max width.
- if symbol.width() as u16 > self.max_line_width {
- continue;
- }
-
- // Break on newline and discard it.
- if symbol == "\n" {
- break;
- }
-
- if current_line_width + symbol.width() as u16 > self.max_line_width {
- // Exhaust the remainder of the line.
- skip_rest = true;
- break;
- }
-
- let symbol = if horizontal_offset == 0 {
- symbol
- } else {
- let w = symbol.width();
- if w > horizontal_offset {
- let t = trim_offset(symbol, horizontal_offset);
- horizontal_offset = 0;
- t
- } else {
- horizontal_offset -= w;
- ""
- }
- };
- current_line_width += symbol.width() as u16;
- self.current_line.push(StyledGrapheme { symbol, style });
- }
-
- if skip_rest {
- for StyledGrapheme { symbol, .. } in &mut self.symbols {
- if symbol == "\n" {
- break;
- }
- }
- }
-
- if symbols_exhausted && self.current_line.is_empty() {
- None
- } else {
- Some((&self.current_line[..], current_line_width))
- }
- }
-}
-
-/// This function will return a str slice which start at specified offset.
-/// As src is a unicode str, start offset has to be calculated with each character.
-fn trim_offset(src: &str, mut offset: usize) -> &str {
- let mut start = 0;
- for c in UnicodeSegmentation::graphemes(src, true) {
- let w = c.width();
- if w <= offset {
- offset -= w;
- start += c.len();
- } else {
- break;
- }
- }
- &src[start..]
-}
-
-#[cfg(test)]
-mod test {
- use crate::tui::style::Style;
-
- use super::*;
- use unicode_segmentation::UnicodeSegmentation;
-
- #[derive(Clone, Copy)]
- enum Composer {
- WordWrapper { trim: bool },
- LineTruncator,
- }
-
- fn run_composer(which: Composer, text: &str, text_area_width: u16) -> (Vec<String>, Vec<u16>) {
- let style = Style::default();
- let mut styled =
- UnicodeSegmentation::graphemes(text, true).map(|g| StyledGrapheme { symbol: g, style });
- let mut composer: Box<dyn LineComposer> = match which {
- Composer::WordWrapper { trim } => {
- Box::new(WordWrapper::new(&mut styled, text_area_width, trim))
- }
- Composer::LineTruncator => Box::new(LineTruncator::new(&mut styled, text_area_width)),
- };
- let mut lines = vec![];
- let mut widths = vec![];
- while let Some((styled, width)) = composer.next_line() {
- let line = styled
- .iter()
- .map(|StyledGrapheme { symbol, .. }| *symbol)
- .collect::<String>();
- assert!(width <= text_area_width);
- lines.push(line);
- widths.push(width);
- }
- (lines, widths)
- }
-
- #[test]
- fn line_composer_one_line() {
- let width = 40;
- for i in 1..width {
- let text = "a".repeat(i);
- let (word_wrapper, _) =
- run_composer(Composer::WordWrapper { trim: true }, &text, width as u16);
- let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width as u16);
- let expected = vec![text];
- assert_eq!(word_wrapper, expected);
- assert_eq!(line_truncator, expected);
- }
- }
-
- #[test]
- fn line_composer_short_lines() {
- let width = 20;
- let text =
- "abcdefg\nhijklmno\npabcdefg\nhijklmn\nopabcdefghijk\nlmnopabcd\n\n\nefghijklmno";
- let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
- let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
-
- let wrapped: Vec<&str> = text.split('\n').collect();
- assert_eq!(word_wrapper, wrapped);
- assert_eq!(line_truncator, wrapped);
- }
-
- #[test]
- fn line_composer_long_word() {
- let width = 20;
- let text = "abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmno";
- let (word_wrapper, _) =
- run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
- let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
-
- let wrapped = vec![
- &text[..width],
- &text[width..width * 2],
- &text[width * 2..width * 3],
- &text[width * 3..],
- ];
- assert_eq!(
- word_wrapper, wrapped,
- "WordWrapper should detect the line cannot be broken on word boundary and \
- break it at line width limit."
- );
- assert_eq!(line_truncator, vec![&text[..width]]);
- }
-
- #[test]
- fn line_composer_long_sentence() {
- let width = 20;
- let text =
- "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l m n o";
- let text_multi_space =
- "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l \
- m n o";
- let (word_wrapper_single_space, _) =
- run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
- let (word_wrapper_multi_space, _) = run_composer(
- Composer::WordWrapper { trim: true },
- text_multi_space,
- width as u16,
- );
- let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
-
- let word_wrapped = vec![
- "abcd efghij",
- "klmnopabcd efgh",
- "ijklmnopabcdefg",
- "hijkl mnopab c d e f",
- "g h i j k l m n o",
- ];
- assert_eq!(word_wrapper_single_space, word_wrapped);
- assert_eq!(word_wrapper_multi_space, word_wrapped);
-
- assert_eq!(line_truncator, vec![&text[..width]]);
- }
-
- #[test]
- fn line_composer_zero_width() {
- let width = 0;
- let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
- let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
- let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
-
- let expected: Vec<&str> = Vec::new();
- assert_eq!(word_wrapper, expected);
- assert_eq!(line_truncator, expected);
- }
-
- #[test]
- fn line_composer_max_line_width_of_1() {
- let width = 1;
- let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
- let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
- let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
-
- let expected: Vec<&str> = UnicodeSegmentation::graphemes(text, true)
- .filter(|g| g.chars().any(|c| !c.is_whitespace()))
- .collect();
- assert_eq!(word_wrapper, expected);
- assert_eq!(line_truncator, vec!["a"]);
- }
-
- #[test]
- fn line_composer_max_line_width_of_1_double_width_characters() {
- let width = 1;
- let text = "コンピュータ上で文字を扱う場合、典型的には文字\naaaによる通信を行う場合にその\
- 両端点では、";
- let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
- let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
- assert_eq!(word_wrapper, vec!["", "a", "a", "a"]);
- assert_eq!(line_truncator, vec!["", "a"]);
- }
-
- /// Tests `WordWrapper` with words some of which exceed line length and some not.
- #[test]
- fn line_composer_word_wrapper_mixed_length() {
- let width = 20;
- let text = "abcd efghij klmnopabcdefghijklmnopabcdefghijkl mnopab cdefghi j klmno";
- let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
- assert_eq!(
- word_wrapper,
- vec![
- "abcd efghij",
- "klmnopabcdefghijklmn",
- "opabcdefghijkl",
- "mnopab cdefghi j",
- "klmno",
- ]
- );
- }
-
- #[test]
- fn line_composer_double_width_chars() {
- let width = 20;
- let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\
- では、";
- let (word_wrapper, word_wrapper_width) =
- run_composer(Composer::WordWrapper { trim: true }, text, width);
- let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
- assert_eq!(line_truncator, vec!["コンピュータ上で文字"]);
- let wrapped = vec![
- "コンピュータ上で文字",
- "を扱う場合、典型的に",
- "は文字による通信を行",
- "う場合にその両端点で",
- "は、",
- ];
- assert_eq!(word_wrapper, wrapped);
- assert_eq!(word_wrapper_width, vec![width, width, width, width, 4]);
- }
-
- #[test]
- fn line_composer_leading_whitespace_removal() {
- let width = 20;
- let text = "AAAAAAAAAAAAAAAAAAAA AAA";
- let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
- let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
- assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", "AAA",]);
- assert_eq!(line_truncator, vec!["AAAAAAAAAAAAAAAAAAAA"]);
- }
-
- /// Tests truncation of leading whitespace.
- #[test]
- fn line_composer_lots_of_spaces() {
- let width = 20;
- let text = " ";
- let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
- let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
- assert_eq!(word_wrapper, vec![""]);
- assert_eq!(line_truncator, vec![" "]);
- }
-
- /// Tests an input starting with a letter, folowed by spaces - some of the behaviour is
- /// incidental.
- #[test]
- fn line_composer_char_plus_lots_of_spaces() {
- let width = 20;
- let text = "a ";
- let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
- let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
- // What's happening below is: the first line gets consumed, trailing spaces discarded,
- // after 20 of which a word break occurs (probably shouldn't). The second line break
- // discards all whitespace. The result should probably be vec!["a"] but it doesn't matter
- // that much.
- assert_eq!(word_wrapper, vec!["a", ""]);
- assert_eq!(line_truncator, vec!["a "]);
- }
-
- #[test]
- fn line_composer_word_wrapper_double_width_chars_mixed_with_spaces() {
- let width = 20;
- // Japanese seems not to use spaces but we should break on spaces anyway... We're using it
- // to test double-width chars.
- // You are more than welcome to add word boundary detection based of alterations of
- // hiragana and katakana...
- // This happens to also be a test case for mixed width because regular spaces are single width.
- let text = "コンピュ ータ上で文字を扱う場合、 典型的には文 字による 通信を行 う場合にその両端点では、";
- let (word_wrapper, word_wrapper_width) =
- run_composer(Composer::WordWrapper { trim: true }, text, width);
- assert_eq!(
- word_wrapper,
- vec![
- "コンピュ",
- "ータ上で文字を扱う場",
- "合、 典型的には文",
- "字による 通信を行",
- "う場合にその両端点で",
- "は、",
- ]
- );
- // Odd-sized lines have a space in them.
- assert_eq!(word_wrapper_width, vec![8, 20, 17, 17, 20, 4]);
- }
-
- /// Ensure words separated by nbsp are wrapped as if they were a single one.
- #[test]
- fn line_composer_word_wrapper_nbsp() {
- let width = 20;
- let text = "AAAAAAAAAAAAAAA AAAA\u{00a0}AAA";
- let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
- assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAA", "AAAA\u{00a0}AAA",]);
-
- // Ensure that if the character was a regular space, it would be wrapped differently.
- let text_space = text.replace('\u{00a0}', " ");
- let (word_wrapper_space, _) =
- run_composer(Composer::WordWrapper { trim: true }, &text_space, width);
- assert_eq!(word_wrapper_space, vec!["AAAAAAAAAAAAAAA AAAA", "AAA",]);
- }
-
- #[test]
- fn line_composer_word_wrapper_preserve_indentation() {
- let width = 20;
- let text = "AAAAAAAAAAAAAAAAAAAA AAA";
- let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
- assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", " AAA",]);
- }
-
- #[test]
- fn line_composer_word_wrapper_preserve_indentation_with_wrap() {
- let width = 10;
- let text = "AAA AAA AAAAA AA AAAAAA\n B\n C\n D";
- let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
- assert_eq!(
- word_wrapper,
- vec!["AAA AAA", "AAAAA AA", "AAAAAA", " B", " C", " D"]
- );
- }
-
- #[test]
- fn line_composer_word_wrapper_preserve_indentation_lots_of_whitespace() {
- let width = 10;
- let text = " 4 Indent\n must wrap!";
- let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
- assert_eq!(
- word_wrapper,
- vec![
- " ",
- " 4",
- "Indent",
- " ",
- " must",
- "wrap!"
- ]
- );
- }
-}