aboutsummaryrefslogtreecommitdiff
path: root/helix-tui/src/widgets/list.rs
diff options
context:
space:
mode:
Diffstat (limited to 'helix-tui/src/widgets/list.rs')
-rw-r--r--helix-tui/src/widgets/list.rs249
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);
+ }
+}