From a515b06bcb556c1be2d0fc3095cd778d413fe40d Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Fri, 31 Mar 2023 22:57:37 +0100 Subject: Vendor ratatui temporarily (#835) * Vendor ratatui temporarily Once https://github.com/tui-rs-revival/ratatui/pull/114 has been merged, we can undo this! But otherwise we can't publish to crates.io with a git dependency. * make tests pass * Shush. * these literally just fail in nix, nowhere else idk how to work with nix properly, and they're also not our tests --- src/ratatui/widgets/list.rs | 268 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 268 insertions(+) create mode 100644 src/ratatui/widgets/list.rs (limited to 'src/ratatui/widgets/list.rs') diff --git a/src/ratatui/widgets/list.rs b/src/ratatui/widgets/list.rs new file mode 100644 index 00000000..9608d299 --- /dev/null +++ b/src/ratatui/widgets/list.rs @@ -0,0 +1,268 @@ +use crate::ratatui::{ + buffer::Buffer, + layout::{Corner, Rect}, + style::Style, + text::Text, + widgets::{Block, StatefulWidget, Widget}, +}; +use unicode_width::UnicodeWidthStr; + +#[derive(Debug, Clone, Default)] +pub struct ListState { + offset: usize, + selected: Option, +} + +impl ListState { + pub fn selected(&self) -> Option { + self.selected + } + + pub fn select(&mut self, index: Option) { + self.selected = index; + if index.is_none() { + self.offset = 0; + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ListItem<'a> { + content: Text<'a>, + style: Style, +} + +impl<'a> ListItem<'a> { + pub fn new(content: T) -> ListItem<'a> + where + T: Into>, + { + 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() + } + + pub fn width(&self) -> usize { + self.content.width() + } +} + +/// A widget to display several items among which one can be selected (optional) +/// +/// # Examples +/// +/// ``` +/// # use ratatui::widgets::{Block, Borders, List, ListItem}; +/// # use ratatui::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>, + items: Vec>, + /// 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>, + /// Whether to repeat the highlight symbol for each line of the selected item + repeat_highlight_symbol: bool, +} + +impl<'a> List<'a> { + pub fn new(items: T) -> List<'a> + where + T: Into>>, + { + List { + block: None, + style: Style::default(), + items: items.into(), + start_corner: Corner::TopLeft, + highlight_style: Style::default(), + highlight_symbol: None, + repeat_highlight_symbol: false, + } + } + + 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 repeat_highlight_symbol(mut self, repeat: bool) -> List<'a> { + self.repeat_highlight_symbol = repeat; + self + } + + pub fn start_corner(mut self, corner: Corner) -> List<'a> { + self.start_corner = corner; + self + } + + fn get_items_bounds( + &self, + selected: Option, + offset: usize, + max_height: usize, + ) -> (usize, usize) { + let offset = offset.min(self.items.len().saturating_sub(1)); + let mut start = offset; + let mut end = offset; + let mut height = 0; + for item in self.items.iter().skip(offset) { + if height + item.height() > max_height { + break; + } + height += item.height(); + end += 1; + } + + let selected = selected.unwrap_or(0).min(self.items.len() - 1); + while selected >= end { + height = height.saturating_add(self.items[end].height()); + end += 1; + while height > max_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 > max_height { + end -= 1; + height = height.saturating_sub(self.items[end].height()); + } + } + (start, end) + } +} + +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 (start, end) = self.get_items_bounds(state.selected, state.offset, list_height); + state.offset = start; + + let highlight_symbol = self.highlight_symbol.unwrap_or(""); + let blank_symbol = " ".repeat(highlight_symbol.width()); + + 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); + for (j, line) in item.content.lines.iter().enumerate() { + // if the item is selected, we need to display the highlight symbol: + // - either for the first line of the item only, + // - or for each line of the item if the appropriate option is set + let symbol = if is_selected && (j == 0 || self.repeat_highlight_symbol) { + highlight_symbol + } else { + &blank_symbol + }; + let (elem_x, max_element_width) = if has_selection { + let (elem_x, _) = buf.set_stringn( + x, + y + j as u16, + symbol, + list_area.width as usize, + item_style, + ); + (elem_x, (list_area.width - (elem_x - x))) + } else { + (x, list_area.width) + }; + buf.set_spans(elem_x, y + j as u16, line, max_element_width); + } + 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); + } +} -- cgit v1.3.1