diff options
Diffstat (limited to 'helix-tui/src/widgets/list.rs')
-rw-r--r-- | helix-tui/src/widgets/list.rs | 249 |
1 files changed, 249 insertions, 0 deletions
diff --git a/helix-tui/src/widgets/list.rs b/helix-tui/src/widgets/list.rs new file mode 100644 index 00000000..e913ce78 --- /dev/null +++ b/helix-tui/src/widgets/list.rs @@ -0,0 +1,249 @@ +use crate::{ + buffer::Buffer, + layout::{Corner, Rect}, + style::Style, + text::Text, + widgets::{Block, StatefulWidget, Widget}, +}; +use std::iter::{self, Iterator}; +use unicode_width::UnicodeWidthStr; + +#[derive(Debug, Clone)] +pub struct ListState { + offset: usize, + selected: Option<usize>, +} + +impl Default for ListState { + fn default() -> ListState { + ListState { + offset: 0, + selected: None, + } + } +} + +impl ListState { + pub fn selected(&self) -> Option<usize> { + self.selected + } + + pub fn select(&mut self, index: Option<usize>) { + self.selected = index; + if index.is_none() { + self.offset = 0; + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ListItem<'a> { + content: Text<'a>, + style: Style, +} + +impl<'a> ListItem<'a> { + pub fn new<T>(content: T) -> ListItem<'a> + where + T: Into<Text<'a>>, + { + ListItem { + content: content.into(), + style: Style::default(), + } + } + + pub fn style(mut self, style: Style) -> ListItem<'a> { + self.style = style; + self + } + + pub fn height(&self) -> usize { + self.content.height() + } +} + +/// A widget to display several items among which one can be selected (optional) +/// +/// # Examples +/// +/// ``` +/// # use helix_tui::widgets::{Block, Borders, List, ListItem}; +/// # use helix_tui::style::{Style, Color, Modifier}; +/// let items = [ListItem::new("Item 1"), ListItem::new("Item 2"), ListItem::new("Item 3")]; +/// List::new(items) +/// .block(Block::default().title("List").borders(Borders::ALL)) +/// .style(Style::default().fg(Color::White)) +/// .highlight_style(Style::default().add_modifier(Modifier::ITALIC)) +/// .highlight_symbol(">>"); +/// ``` +#[derive(Debug, Clone)] +pub struct List<'a> { + block: Option<Block<'a>>, + items: Vec<ListItem<'a>>, + /// Style used as a base style for the widget + style: Style, + start_corner: Corner, + /// Style used to render selected item + highlight_style: Style, + /// Symbol in front of the selected item (Shift all items to the right) + highlight_symbol: Option<&'a str>, +} + +impl<'a> List<'a> { + pub fn new<T>(items: T) -> List<'a> + where + T: Into<Vec<ListItem<'a>>>, + { + List { + block: None, + style: Style::default(), + items: items.into(), + start_corner: Corner::TopLeft, + highlight_style: Style::default(), + highlight_symbol: None, + } + } + + pub fn block(mut self, block: Block<'a>) -> List<'a> { + self.block = Some(block); + self + } + + pub fn style(mut self, style: Style) -> List<'a> { + self.style = style; + self + } + + pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> List<'a> { + self.highlight_symbol = Some(highlight_symbol); + self + } + + pub fn highlight_style(mut self, style: Style) -> List<'a> { + self.highlight_style = style; + self + } + + pub fn start_corner(mut self, corner: Corner) -> List<'a> { + self.start_corner = corner; + self + } +} + +impl<'a> StatefulWidget for List<'a> { + type State = ListState; + + fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + buf.set_style(area, self.style); + let list_area = match self.block.take() { + Some(b) => { + let inner_area = b.inner(area); + b.render(area, buf); + inner_area + } + None => area, + }; + + if list_area.width < 1 || list_area.height < 1 { + return; + } + + if self.items.is_empty() { + return; + } + let list_height = list_area.height as usize; + + let mut start = state.offset; + let mut end = state.offset; + let mut height = 0; + for item in self.items.iter().skip(state.offset) { + if height + item.height() > list_height { + break; + } + height += item.height(); + end += 1; + } + + let selected = state.selected.unwrap_or(0).min(self.items.len() - 1); + while selected >= end { + height = height.saturating_add(self.items[end].height()); + end += 1; + while height > list_height { + height = height.saturating_sub(self.items[start].height()); + start += 1; + } + } + while selected < start { + start -= 1; + height = height.saturating_add(self.items[start].height()); + while height > list_height { + end -= 1; + height = height.saturating_sub(self.items[end].height()); + } + } + state.offset = start; + + let highlight_symbol = self.highlight_symbol.unwrap_or(""); + let blank_symbol = iter::repeat(" ") + .take(highlight_symbol.width()) + .collect::<String>(); + + let mut current_height = 0; + let has_selection = state.selected.is_some(); + for (i, item) in self + .items + .iter_mut() + .enumerate() + .skip(state.offset) + .take(end - start) + { + let (x, y) = match self.start_corner { + Corner::BottomLeft => { + current_height += item.height() as u16; + (list_area.left(), list_area.bottom() - current_height) + } + _ => { + let pos = (list_area.left(), list_area.top() + current_height); + current_height += item.height() as u16; + pos + } + }; + let area = Rect { + x, + y, + width: list_area.width, + height: item.height() as u16, + }; + let item_style = self.style.patch(item.style); + buf.set_style(area, item_style); + + let is_selected = state.selected.map(|s| s == i).unwrap_or(false); + let elem_x = if has_selection { + let symbol = if is_selected { + highlight_symbol + } else { + &blank_symbol + }; + let (x, _) = buf.set_stringn(x, y, symbol, list_area.width as usize, item_style); + x + } else { + x + }; + let max_element_width = (list_area.width - (elem_x - x)) as usize; + for (j, line) in item.content.lines.iter().enumerate() { + buf.set_spans(elem_x, y + j as u16, line, max_element_width as u16); + } + if is_selected { + buf.set_style(area, self.highlight_style); + } + } + } +} + +impl<'a> Widget for List<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + let mut state = ListState::default(); + StatefulWidget::render(self, area, buf, &mut state); + } +} |