diff options
| author | Conrad Ludgate <conradludgate@gmail.com> | 2023-03-23 09:19:29 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-03-23 09:19:29 +0000 |
| commit | ba1d615f5e5d904a3bd7a7f5f0ce336b89995aea (patch) | |
| tree | db84efdef064426a9c2e32b897bb39c06fe4145e /src/tui/widgets | |
| parent | Allow changing search_mode during interactive search (#586) (diff) | |
| download | atuin-ba1d615f5e5d904a3bd7a7f5f0ce336b89995aea.zip | |
chore: remove tui vendoring (#804)
Diffstat (limited to 'src/tui/widgets')
| -rw-r--r-- | src/tui/widgets/block.rs | 562 | ||||
| -rw-r--r-- | src/tui/widgets/mod.rs | 159 | ||||
| -rw-r--r-- | src/tui/widgets/paragraph.rs | 194 | ||||
| -rw-r--r-- | src/tui/widgets/reflow.rs | 537 |
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!" - ] - ); - } -} |
